From 124d0d4ec45e57b269d32fc3d92069413b4294c0 Mon Sep 17 00:00:00 2001 From: Lunny Xiao Date: Thu, 13 Nov 2025 18:04:18 -0800 Subject: [PATCH 1/2] Support ForceWithLease push option --- modules/git/repo.go | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/modules/git/repo.go b/modules/git/repo.go index 7e86b10de9bd3..88acbd30e6000 100644 --- a/modules/git/repo.go +++ b/modules/git/repo.go @@ -186,18 +186,21 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error { // PushOptions options when push to remote type PushOptions struct { - Remote string - Branch string - Force bool - Mirror bool - Env []string - Timeout time.Duration + Remote string + Branch string + Force bool + ForceWithLease string + Mirror bool + Env []string + Timeout time.Duration } // Push pushs local commits to given remote branch. func Push(ctx context.Context, repoPath string, opts PushOptions) error { cmd := gitcmd.NewCommand("push") - if opts.Force { + if opts.ForceWithLease != "" { + cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease) + } else if opts.Force { cmd.AddArguments("-f") } if opts.Mirror { From b8597b76b2c9e352610f34e4d126378a06c323a8 Mon Sep 17 00:00:00 2001 From: Michal Suchanek Date: Sat, 6 Dec 2025 12:16:57 +0100 Subject: [PATCH 2/2] Add UpdateBranch API (#35368). This is CreateBranch for branches that already exist making it possible to reset branch to an arbitrary commit. Consistent with CreateFile/UpdateFile new branches are created by POST and existing branches updated by PUT. --- assets/go-licenses.json | 5 + go.mod | 1 + go.sum | 2 + modules/structs/repo.go | 24 +++++ routers/api/v1/api.go | 1 + routers/api/v1/repo/branch.go | 70 ++++++++++++-- routers/api/v1/swagger/options.go | 3 + routers/web/repo/branch.go | 4 +- services/repository/branch.go | 33 +++++-- templates/swagger/v1_json.tmpl | 86 +++++++++++++++++ tests/integration/actions_trigger_test.go | 8 +- tests/integration/api_branch_test.go | 92 +++++++++++++++++-- .../api_repo_get_contents_list_test.go | 2 +- .../integration/api_repo_get_contents_test.go | 2 +- 14 files changed, 301 insertions(+), 32 deletions(-) diff --git a/assets/go-licenses.json b/assets/go-licenses.json index b105757683141..05e19a8747307 100644 --- a/assets/go-licenses.json +++ b/assets/go-licenses.json @@ -749,6 +749,11 @@ "path": "github.com/jhillyerd/enmime/LICENSE", "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2012-2016 James Hillyerd, All Rights Reserved\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" }, + { + "name": "github.com/jinzhu/copier", + "path": "github.com/jinzhu/copier/License", + "licenseText": "The MIT License (MIT)\n\nCopyright (c) 2015 Jinzhu\n\nPermission is hereby granted, free of charge, to any person obtaining a copy of\nthis software and associated documentation files (the \"Software\"), to deal in\nthe Software without restriction, including without limitation the rights to\nuse, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of\nthe Software, and to permit persons to whom the Software is furnished to do so,\nsubject to the following conditions:\n\nThe above copyright notice and this permission notice shall be included in all\ncopies or substantial portions of the Software.\n\nTHE SOFTWARE IS PROVIDED \"AS IS\", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR\nIMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS\nFOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR\nCOPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER\nIN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN\nCONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.\n" + }, { "name": "github.com/josharian/intern", "path": "github.com/josharian/intern/license.md", diff --git a/go.mod b/go.mod index 6806e76ffc5e8..9fe8cf02b59d2 100644 --- a/go.mod +++ b/go.mod @@ -77,6 +77,7 @@ require ( github.com/huandu/xstrings v1.5.0 github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056 github.com/jhillyerd/enmime v1.3.0 + github.com/jinzhu/copier v0.4.0 github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51 github.com/klauspost/compress v1.18.0 github.com/klauspost/cpuid/v2 v2.3.0 diff --git a/go.sum b/go.sum index 86fe782ae7ba7..f914b519ce389 100644 --- a/go.sum +++ b/go.sum @@ -510,6 +510,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc= github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw= github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs= +github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8= +github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg= github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY= github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= diff --git a/modules/structs/repo.go b/modules/structs/repo.go index c1c85837fc89e..c5d0635622e63 100644 --- a/modules/structs/repo.go +++ b/modules/structs/repo.go @@ -292,6 +292,30 @@ type RenameBranchRepoOption struct { Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"` } +// UpdateBranchRepoOption options when updating a branch reference in a repository +// swagger:model +type UpdateBranchRepoOption struct { + // Name of the branch to update + // + // required: true + // unique: true + BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"` + + // the commit ID (SHA) for the branch that already exists to update + SHA string `json:"sha" binding:"Required"` + + // Deprecated: true + // Name of the old branch to reset to + // + // unique: true + OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"` + + // Name of the old branch/tag/commit to reset to + // + // unique: true + OldRefName string `json:"old_ref_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 8e07685759803..815459a4de01c 100644 --- a/routers/api/v1/api.go +++ b/routers/api/v1/api.go @@ -1242,6 +1242,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.Put("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch) m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.RenameBranchRepoOption{}), repo.RenameBranch) }, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode)) m.Group("/branch_protections", func() { diff --git a/routers/api/v1/repo/branch.go b/routers/api/v1/repo/branch.go index b9060e9cbd09f..6b3c3ae76a1fe 100644 --- a/routers/api/v1/repo/branch.go +++ b/routers/api/v1/repo/branch.go @@ -6,6 +6,7 @@ package repo import ( "errors" + "fmt" "net/http" "code.gitea.io/gitea/models/db" @@ -26,6 +27,8 @@ import ( pull_service "code.gitea.io/gitea/services/pull" release_service "code.gitea.io/gitea/services/release" repo_service "code.gitea.io/gitea/services/repository" + + "github.com/jinzhu/copier" ) // GetBranch get a branch of a repository @@ -203,6 +206,62 @@ func CreateBranch(ctx *context.APIContext) { // "423": // "$ref": "#/responses/repoArchivedError" + optCreate := web.GetForm(ctx).(*api.CreateBranchRepoOption) + opt := api.UpdateBranchRepoOption{} + err := copier.Copy(&opt, optCreate) + if err != nil { + ctx.APIError(http.StatusInternalServerError, fmt.Sprintf("Error processing request %s.", err)) + return + } + + CreateUpdateRepoBranch(ctx, &opt) +} + +// UpdateBranch update (reset) a branch in a user's repository +func UpdateBranch(ctx *context.APIContext) { + // swagger:operation PUT /repos/{owner}/{repo}/branches 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: body + // in: body + // schema: + // "$ref": "#/definitions/UpdateBranchRepoOption" + // responses: + // "201": + // "$ref": "#/responses/Branch" + // "403": + // description: The branch is archived or a mirror. + // "404": + // description: The branch does not exist. + // "409": + // description: The branch SHA does not match. + // "423": + // "$ref": "#/responses/repoArchivedError" + + opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption) + + CreateUpdateRepoBranch(ctx, opt) +} + +func CreateUpdateRepoBranch(ctx *context.APIContext, opt *api.UpdateBranchRepoOption) { + var oldCommit *git.Commit + var err error + if ctx.Repo.Repository.IsEmpty { ctx.APIError(http.StatusNotFound, "Git Repository is empty.") return @@ -213,11 +272,6 @@ func CreateBranch(ctx *context.APIContext) { return } - opt := web.GetForm(ctx).(*api.CreateBranchRepoOption) - - var oldCommit *git.Commit - var err error - if len(opt.OldRefName) > 0 { oldCommit, err = ctx.Repo.GitRepo.GetCommit(opt.OldRefName) if err != nil { @@ -243,14 +297,16 @@ func CreateBranch(ctx *context.APIContext) { } } - err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName) + err = repo_service.CreateUpdateBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName, opt.SHA) if err != nil { if git_model.IsErrBranchNotExist(err) { ctx.APIError(http.StatusNotFound, "The old branch does not exist") } else if release_service.IsErrTagAlreadyExists(err) { ctx.APIError(http.StatusConflict, "The branch with the same tag already exists.") - } else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) { + } else if git_model.IsErrBranchAlreadyExists(err) { ctx.APIError(http.StatusConflict, "The branch already exists.") + } else if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { + ctx.APIError(http.StatusConflict, "The branch SHA does not match.") } else if git_model.IsErrBranchNameConflict(err) { ctx.APIError(http.StatusConflict, "The branch with the same name already exists.") } else { diff --git a/routers/api/v1/swagger/options.go b/routers/api/v1/swagger/options.go index b80a9c14ba027..9cf84b98ffa29 100644 --- a/routers/api/v1/swagger/options.go +++ b/routers/api/v1/swagger/options.go @@ -148,6 +148,9 @@ type swaggerParameterBodies struct { // in:body CreateBranchRepoOption api.CreateBranchRepoOption + // in:body + UpdateBranchRepoOption api.UpdateBranchRepoOption + // in:body CreateBranchProtectionOption api.CreateBranchProtectionOption diff --git a/routers/web/repo/branch.go b/routers/web/repo/branch.go index f21f5682318a6..853a928d7f812 100644 --- a/routers/web/repo/branch.go +++ b/routers/web/repo/branch.go @@ -194,9 +194,9 @@ func CreateBranch(ctx *context.Context) { } err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "") } else if ctx.Repo.RefFullName.IsBranch() { - err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName) + err = repo_service.CreateUpdateBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName, "") } else { - err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName) + err = repo_service.CreateUpdateBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName, "") } if err != nil { if release_service.IsErrProtectedTagName(err) { diff --git a/services/repository/branch.go b/services/repository/branch.go index 0a2fd30620d22..9bc4e5daa5836 100644 --- a/services/repository/branch.go +++ b/services/repository/branch.go @@ -37,14 +37,14 @@ import ( "xorm.io/builder" ) -// CreateNewBranch creates a new repository branch -func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName string) (err error) { +// CreateUpdateBranch creates or updates a repository branch +func CreateUpdateBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName, sha string) (err error) { branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName) if err != nil { return err } - return CreateNewBranchFromCommit(ctx, doer, repo, branch.CommitID, branchName) + return CreateUpdateBranchFromCommit(ctx, doer, repo, branch.CommitID, branchName, sha) } // Branch contains the branch information @@ -373,23 +373,36 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames, }) } -// CreateNewBranchFromCommit creates a new repository branch -func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitID, branchName string) (err error) { +// CreateUpdateBranchFromCommit creates or updates a repository branch +func CreateUpdateBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitID, branchName, sha string) (err error) { err = repo.MustNotBeArchived() if err != nil { return err } - // Check if branch name can be used - if err := checkBranchName(ctx, repo, branchName); err != nil { - return err + if sha != "" { + _, err := git_model.GetBranch(ctx, repo.ID, branchName) + if err != nil { + return err + } + } else { + // Check if branch name can be used + if err := checkBranchName(ctx, repo, branchName); err != nil { + return err + } } - if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{ + pushOpts := git.PushOptions{ Remote: repo.RepoPath(), Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName), Env: repo_module.PushingEnvironment(doer, repo), - }); err != nil { + } + + if sha != "" { + pushOpts.ForceWithLease = fmt.Sprintf("%s:%s", git.BranchPrefix+branchName, sha) + } + + if err := git.Push(ctx, repo.RepoPath(), pushOpts); err != nil { if git.IsErrPushOutOfDate(err) || git.IsErrPushRejected(err) { return err } diff --git a/templates/swagger/v1_json.tmpl b/templates/swagger/v1_json.tmpl index 0cefa6795f4f5..0d1dd4bf3164b 100644 --- a/templates/swagger/v1_json.tmpl +++ b/templates/swagger/v1_json.tmpl @@ -6654,6 +6654,59 @@ } } }, + "put": { + "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 + }, + { + "name": "body", + "in": "body", + "schema": { + "$ref": "#/definitions/UpdateBranchRepoOption" + } + } + ], + "responses": { + "201": { + "$ref": "#/responses/Branch" + }, + "403": { + "description": "The branch is archived or a mirror." + }, + "404": { + "description": "The branch does not exist." + }, + "409": { + "description": "The branch SHA does not match." + }, + "423": { + "$ref": "#/responses/repoArchivedError" + } + } + }, "post": { "consumes": [ "application/json" @@ -28702,6 +28755,39 @@ }, "x-go-package": "code.gitea.io/gitea/modules/structs" }, + "UpdateBranchRepoOption": { + "description": "UpdateBranchRepoOption options when updating a branch reference in a repository", + "type": "object", + "required": [ + "new_branch_name" + ], + "properties": { + "new_branch_name": { + "description": "Name of the branch to update", + "type": "string", + "uniqueItems": true, + "x-go-name": "BranchName" + }, + "old_branch_name": { + "description": "Deprecated: true\nName of the old branch to reset to", + "type": "string", + "uniqueItems": true, + "x-go-name": "OldBranchName" + }, + "old_ref_name": { + "description": "Name of the old branch/tag/commit to reset to", + "type": "string", + "uniqueItems": true, + "x-go-name": "OldRefName" + }, + "sha": { + "description": "the commit ID (SHA) for the branch that already exists to update", + "type": "string", + "x-go-name": "SHA" + } + }, + "x-go-package": "code.gitea.io/gitea/modules/structs" + }, "UpdateFileOptions": { "description": "UpdateFileOptions options for updating or creating a file\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/actions_trigger_test.go b/tests/integration/actions_trigger_test.go index 9dc0ddb9df8a5..4ae3eef136a21 100644 --- a/tests/integration/actions_trigger_test.go +++ b/tests/integration/actions_trigger_test.go @@ -412,7 +412,7 @@ jobs: assert.NoError(t, err) // create a branch - err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-create-branch") + err = repo_service.CreateUpdateBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-create-branch", "") assert.NoError(t, err) run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ Title: "add workflow", @@ -530,7 +530,7 @@ jobs: // create a new branch testBranch := "test-branch" - err = repo_service.CreateNewBranch(t.Context(), user2, repo, "main", testBranch) + err = repo_service.CreateUpdateBranch(t.Context(), user2, repo, "main", testBranch, "") assert.NoError(t, err) // create Pull @@ -1509,7 +1509,7 @@ jobs: assert.NoError(t, err) // create a branch - err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name-with-variables") + err = repo_service.CreateUpdateBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name-with-variables", "") assert.NoError(t, err) run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ Title: user2.LoginName + " is running this workflow", @@ -1583,7 +1583,7 @@ jobs: assert.NoError(t, err) // create a branch - err = repo_service.CreateNewBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name") + err = repo_service.CreateUpdateBranchFromCommit(t.Context(), user2, repo, branch.CommitID, "test-action-run-name", "") assert.NoError(t, err) run := unittest.AssertExistsAndLoadBean(t, &actions_model.ActionRun{ Title: "run name without variables", diff --git a/tests/integration/api_branch_test.go b/tests/integration/api_branch_test.go index 2147ef9d0d9c7..bfef090e78a7b 100644 --- a/tests/integration/api_branch_test.go +++ b/tests/integration/api_branch_test.go @@ -4,6 +4,7 @@ package integration import ( + "encoding/base64" "net/http" "net/http/httptest" "net/url" @@ -18,14 +19,14 @@ import ( "github.com/stretchr/testify/assert" ) -func testAPIGetBranch(t *testing.T, branchName string, exists bool) { - token := getUserToken(t, "user2", auth_model.AccessTokenScopeReadRepository) - req := NewRequestf(t, "GET", "/api/v1/repos/user2/repo1/branches/%s", branchName). +func testAPIGetBranch(t *testing.T, user, repo, branchName string, exists bool) string { + token := getUserToken(t, user, auth_model.AccessTokenScopeReadRepository) + req := NewRequestf(t, "GET", "/api/v1/repos/"+user+"/"+repo+"/branches/%s", branchName). AddTokenAuth(token) resp := MakeRequest(t, req, NoExpectedStatus) if !exists { assert.Equal(t, http.StatusNotFound, resp.Code) - return + return "" } assert.Equal(t, http.StatusOK, resp.Code) var branch api.Branch @@ -33,6 +34,7 @@ func testAPIGetBranch(t *testing.T, branchName string, exists bool) { assert.Equal(t, branchName, branch.Name) assert.True(t, branch.UserCanPush) assert.True(t, branch.UserCanMerge) + return branch.Commit.ID } func testAPIGetBranchProtection(t *testing.T, branchName string, expectedHTTPStatus int) *api.BranchProtection { @@ -103,7 +105,7 @@ func TestAPIGetBranch(t *testing.T) { {"feature/1", true}, {"feature/1/doesnotexist", false}, } { - testAPIGetBranch(t, test.BranchName, test.Exists) + testAPIGetBranch(t, "user2", "repo1", test.BranchName, test.Exists) } } @@ -113,7 +115,8 @@ func TestAPICreateBranch(t *testing.T) { func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { username := "user2" - ctx := NewAPITestContext(t, username, "my-noo-repo", auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + reponame := "my-noo-repo" + ctx := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) giteaURL.Path = ctx.GitPath() t.Run("CreateRepo", doAPICreateRepository(ctx, false)) @@ -164,7 +167,7 @@ func testAPICreateBranches(t *testing.T, giteaURL *url.URL) { for _, test := range testCases { session := ctx.Session t.Run(test.NewBranch, func(t *testing.T) { - testAPICreateBranch(t, session, "user2", "my-noo-repo", test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus) + testAPICreateBranch(t, session, username, reponame, test.OldBranch, test.NewBranch, test.ExpectedHTTPStatus) }) } } @@ -251,6 +254,81 @@ func testAPIRenameBranch(t *testing.T, doerName, ownerName, repoName, from, to s return MakeRequest(t, req, expectedHTTPStatus) } +func TestAPIUpdateBranch(t *testing.T) { + onGiteaRun(t, testAPIUpdateBranches) +} + +func testAPIUpdateBranches(t *testing.T, giteaURL *url.URL) { + username := "user2" + reponame := "my-nuu-repo" + branchname := "new-branch" + ctx := NewAPITestContext(t, username, reponame, auth_model.AccessTokenScopeWriteRepository, auth_model.AccessTokenScopeWriteUser) + giteaURL.Path = ctx.GitPath() + session := ctx.Session + + t.Run("CreateRepo", doAPICreateRepository(ctx, false)) + + t.Run("create branch", func(t *testing.T) { + testAPICreateBranch(t, session, username, reponame, "", branchname, http.StatusCreated) + }) + + oldCommit := "" + t.Run("get commit ID", func(t *testing.T) { + oldCommit = testAPIGetBranch(t, username, reponame, branchname, true) + }) + t.Run("advance branch", func(t *testing.T) { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "POST", "/api/v1/repos/"+username+"/"+reponame+"/contents/a new file", + &api.CreateFileOptions{ + FileOptions: api.FileOptions{ + BranchName: branchname, + }, + ContentBase64: base64.StdEncoding.EncodeToString([]byte("foo")), + }).AddTokenAuth(token) + resp := MakeRequest(t, req, http.StatusCreated) + assert.Equal(t, http.StatusCreated, resp.Result().StatusCode) + }) + newCommit := "" + t.Run("get new commit ID", func(t *testing.T) { + newCommit = testAPIGetBranch(t, username, reponame, branchname, true) + }) + + t.Run("fail update nonexistent branch", func(t *testing.T) { + testAPIUpdateBranch(t, session, username, reponame, oldCommit, "no-branch-here", oldCommit, http.StatusNotFound) + }) + t.Run("fail update on sha mismatch", func(t *testing.T) { + testAPIUpdateBranch(t, session, username, reponame, oldCommit, branchname, oldCommit, http.StatusConflict) + assert.Equal(t, newCommit, testAPIGetBranch(t, username, reponame, branchname, true)) + }) + t.Run("reset to old commit", func(t *testing.T) { + testAPIUpdateBranch(t, session, username, reponame, oldCommit, branchname, newCommit, http.StatusCreated) + assert.Equal(t, oldCommit, testAPIGetBranch(t, username, reponame, branchname, true)) + }) + t.Run("reset to new commit", func(t *testing.T) { + testAPIUpdateBranch(t, session, username, reponame, newCommit, branchname, oldCommit, http.StatusCreated) + assert.Equal(t, newCommit, testAPIGetBranch(t, username, reponame, branchname, true)) + }) +} + +func testAPIUpdateBranch(t testing.TB, session *TestSession, user, repo, oldBranch, newBranch, sha string, status int) bool { + token := getTokenForLoggedInUser(t, session, auth_model.AccessTokenScopeWriteRepository) + req := NewRequestWithJSON(t, "PUT", "/api/v1/repos/"+user+"/"+repo+"/branches", &api.UpdateBranchRepoOption{ + BranchName: newBranch, + OldRefName: oldBranch, + SHA: sha, + }).AddTokenAuth(token) + resp := MakeRequest(t, req, status) + + var branch api.Branch + DecodeJSON(t, resp, &branch) + + if resp.Result().StatusCode == http.StatusCreated { + assert.Equal(t, newBranch, branch.Name) + } + + return resp.Result().StatusCode == status +} + func TestAPIBranchProtection(t *testing.T) { defer tests.PrepareTestEnv(t)() diff --git a/tests/integration/api_repo_get_contents_list_test.go b/tests/integration/api_repo_get_contents_list_test.go index 4984559f0c603..fe3f8352811a0 100644 --- a/tests/integration/api_repo_get_contents_list_test.go +++ b/tests/integration/api_repo_get_contents_list_test.go @@ -80,7 +80,7 @@ func testAPIGetContentsList(t *testing.T, u *url.URL) { // Make a new branch in repo1 newBranch := "test_branch" - err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch) + err = repo_service.CreateUpdateBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch, "") assert.NoError(t, err) commitID, _ := gitRepo.GetBranchCommitID(repo1.DefaultBranch) diff --git a/tests/integration/api_repo_get_contents_test.go b/tests/integration/api_repo_get_contents_test.go index 6225c96bc6d9b..6f272b44ff5a9 100644 --- a/tests/integration/api_repo_get_contents_test.go +++ b/tests/integration/api_repo_get_contents_test.go @@ -85,7 +85,7 @@ func testAPIGetContents(t *testing.T, u *url.URL) { // Make a new branch in repo1 newBranch := "test_branch" - err = repo_service.CreateNewBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch) + err = repo_service.CreateUpdateBranch(t.Context(), user2, repo1, repo1.DefaultBranch, newBranch, "") require.NoError(t, err) commitID, err := gitRepo.GetBranchCommitID(repo1.DefaultBranch)