Skip to content

Commit d7a492f

Browse files
committed
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.
1 parent c287a8c commit d7a492f

File tree

14 files changed

+327
-26
lines changed

14 files changed

+327
-26
lines changed

go.mod

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,7 @@ require (
7777
github.com/huandu/xstrings v1.5.0
7878
github.com/jaytaylor/html2text v0.0.0-20230321000545-74c2419ad056
7979
github.com/jhillyerd/enmime v1.3.0
80+
github.com/jinzhu/copier v0.4.0
8081
github.com/kballard/go-shellquote v0.0.0-20180428030007-95032a82bc51
8182
github.com/klauspost/compress v1.18.0
8283
github.com/klauspost/cpuid/v2 v2.3.0

go.sum

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -510,6 +510,8 @@ github.com/jcmturner/rpc/v2 v2.0.3 h1:7FXXj8Ti1IaVFpSAziCZWNzbNuZmnvw/i6CqLNdWfZ
510510
github.com/jcmturner/rpc/v2 v2.0.3/go.mod h1:VUJYCIDm3PVOEHw8sgt091/20OJjskO/YJki3ELg/Hc=
511511
github.com/jhillyerd/enmime v1.3.0 h1:LV5kzfLidiOr8qRGIpYYmUZCnhrPbcFAnAFUnWn99rw=
512512
github.com/jhillyerd/enmime v1.3.0/go.mod h1:6c6jg5HdRRV2FtvVL69LjiX1M8oE0xDX9VEhV3oy4gs=
513+
github.com/jinzhu/copier v0.4.0 h1:w3ciUoD19shMCRargcpm0cm91ytaBhDvuRpz1ODO/U8=
514+
github.com/jinzhu/copier v0.4.0/go.mod h1:DfbEm0FYsaqBcKcFuvmOZb218JkPGtvSHsKg8S8hyyg=
513515
github.com/josharian/intern v1.0.0 h1:vlS4z54oSdjm0bgjRigI+G1HpF+tI+9rE5LLzOg8HmY=
514516
github.com/josharian/intern v1.0.0/go.mod h1:5DoeVV0s6jJacbCEi61lwdGj/aVlrQvzHFFd8Hwg//Y=
515517
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=

models/git/branch.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,27 @@ func (err ErrBranchAlreadyExists) Unwrap() error {
6161
return util.ErrAlreadyExist
6262
}
6363

64+
// ErrBranchShaMismatch represents an error that branch points to different SHA.
65+
type ErrBranchShaMismatch struct {
66+
BranchName string
67+
Expected string
68+
Actual string
69+
}
70+
71+
// IsErrBranchShaMismatch checks if an error is an ErrBranchShaMismatch.
72+
func IsErrBranchShaMismatch(err error) bool {
73+
_, ok := err.(ErrBranchShaMismatch)
74+
return ok
75+
}
76+
77+
func (err ErrBranchShaMismatch) Error() string {
78+
return fmt.Sprintf("branch already references %s, expected %s [name: %s]", err.Actual, err.Expected, err.BranchName)
79+
}
80+
81+
func (err ErrBranchShaMismatch) Unwrap() error {
82+
return util.ErrUnprocessableContent
83+
}
84+
6485
// ErrBranchNameConflict represents an error that branch name conflicts with other branch.
6586
type ErrBranchNameConflict struct {
6687
BranchName string

modules/structs/repo.go

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -282,6 +282,30 @@ type CreateBranchRepoOption struct {
282282
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"`
283283
}
284284

285+
// UpdateBranchRepoOption options when updating a branch in a repository
286+
// swagger:model
287+
type UpdateBranchRepoOption struct {
288+
// Name of the branch to create
289+
//
290+
// required: true
291+
// unique: true
292+
BranchName string `json:"new_branch_name" binding:"Required;GitRefName;MaxSize(100)"`
293+
294+
// the commit ID (SHA) for the branch that already exists to update
295+
SHA string `json:"sha" binding:"Required"`
296+
297+
// Deprecated: true
298+
// Name of the old branch to create from
299+
//
300+
// unique: true
301+
OldBranchName string `json:"old_branch_name" binding:"GitRefName;MaxSize(100)"`
302+
303+
// Name of the old branch/tag/commit to create from
304+
//
305+
// unique: true
306+
OldRefName string `json:"old_ref_name" binding:"GitRefName;MaxSize(100)"`
307+
}
308+
285309
// RenameBranchRepoOption options when renaming a branch in a repository
286310
// swagger:model
287311
type RenameBranchRepoOption struct {

routers/api/v1/api.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1242,6 +1242,7 @@ func Routes() *web.Router {
12421242
m.Get("/*", repo.GetBranch)
12431243
m.Delete("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, repo.DeleteBranch)
12441244
m.Post("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.CreateBranchRepoOption{}), repo.CreateBranch)
1245+
m.Put("", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.UpdateBranchRepoOption{}), repo.UpdateBranch)
12451246
m.Patch("/*", reqToken(), reqRepoWriter(unit.TypeCode), mustNotBeArchived, bind(api.RenameBranchRepoOption{}), repo.RenameBranch)
12461247
}, context.ReferencesGitRepo(), reqRepoReader(unit.TypeCode))
12471248
m.Group("/branch_protections", func() {

routers/api/v1/repo/branch.go

Lines changed: 68 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ import (
2626
pull_service "code.gitea.io/gitea/services/pull"
2727
release_service "code.gitea.io/gitea/services/release"
2828
repo_service "code.gitea.io/gitea/services/repository"
29+
30+
"github.com/jinzhu/copier"
2931
)
3032

3133
// GetBranch get a branch of a repository
@@ -213,8 +215,69 @@ func CreateBranch(ctx *context.APIContext) {
213215
return
214216
}
215217

216-
opt := web.GetForm(ctx).(*api.CreateBranchRepoOption)
218+
optCreate := web.GetForm(ctx).(*api.CreateBranchRepoOption)
219+
opt := api.UpdateBranchRepoOption{}
220+
err := copier.Copy(&opt, optCreate)
221+
if err != nil {
222+
ctx.APIError(http.StatusInternalServerError, "Error processing request.")
223+
return
224+
}
225+
226+
CreateUpdateRepoBranch(ctx, &opt)
227+
}
228+
229+
// UpdateBranch update (reset) a branch in a user's repository
230+
func UpdateBranch(ctx *context.APIContext) {
231+
// swagger:operation PUT /repos/{owner}/{repo}/branches repository repoUpdateBranch
232+
// ---
233+
// summary: Update a branch
234+
// consumes:
235+
// - application/json
236+
// produces:
237+
// - application/json
238+
// parameters:
239+
// - name: owner
240+
// in: path
241+
// description: owner of the repo
242+
// type: string
243+
// required: true
244+
// - name: repo
245+
// in: path
246+
// description: name of the repo
247+
// type: string
248+
// required: true
249+
// - name: body
250+
// in: body
251+
// schema:
252+
// "$ref": "#/definitions/UpdateBranchRepoOption"
253+
// responses:
254+
// "201":
255+
// "$ref": "#/responses/Branch"
256+
// "403":
257+
// description: The branch is archived or a mirror.
258+
// "404":
259+
// description: The branch does not exist.
260+
// "409":
261+
// description: The branch SHA does not match.
262+
// "423":
263+
// "$ref": "#/responses/repoArchivedError"
264+
265+
if ctx.Repo.Repository.IsEmpty {
266+
ctx.APIError(http.StatusNotFound, "Git Repository is empty.")
267+
return
268+
}
269+
270+
if ctx.Repo.Repository.IsMirror {
271+
ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.")
272+
return
273+
}
274+
275+
opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption)
276+
277+
CreateUpdateRepoBranch(ctx, opt)
278+
}
217279

280+
func CreateUpdateRepoBranch(ctx *context.APIContext, opt *api.UpdateBranchRepoOption) {
218281
var oldCommit *git.Commit
219282
var err error
220283

@@ -243,14 +306,16 @@ func CreateBranch(ctx *context.APIContext) {
243306
}
244307
}
245308

246-
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName)
309+
err = repo_service.CreateUpdateBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, oldCommit.ID.String(), opt.BranchName, opt.SHA)
247310
if err != nil {
248311
if git_model.IsErrBranchNotExist(err) {
249312
ctx.APIError(http.StatusNotFound, "The old branch does not exist")
250313
} else if release_service.IsErrTagAlreadyExists(err) {
251314
ctx.APIError(http.StatusConflict, "The branch with the same tag already exists.")
252-
} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
315+
} else if git_model.IsErrBranchAlreadyExists(err) {
253316
ctx.APIError(http.StatusConflict, "The branch already exists.")
317+
} else if git_model.IsErrBranchShaMismatch(err) || git.IsErrPushOutOfDate(err) {
318+
ctx.APIError(http.StatusConflict, "The branch SHA does not match.")
254319
} else if git_model.IsErrBranchNameConflict(err) {
255320
ctx.APIError(http.StatusConflict, "The branch with the same name already exists.")
256321
} else {

routers/api/v1/swagger/options.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,9 @@ type swaggerParameterBodies struct {
148148
// in:body
149149
CreateBranchRepoOption api.CreateBranchRepoOption
150150

151+
// in:body
152+
UpdateBranchRepoOption api.UpdateBranchRepoOption
153+
151154
// in:body
152155
CreateBranchProtectionOption api.CreateBranchProtectionOption
153156

routers/web/repo/branch.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -194,9 +194,9 @@ func CreateBranch(ctx *context.Context) {
194194
}
195195
err = release_service.CreateNewTag(ctx, ctx.Doer, ctx.Repo.Repository, target, form.NewBranchName, "")
196196
} else if ctx.Repo.RefFullName.IsBranch() {
197-
err = repo_service.CreateNewBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName)
197+
err = repo_service.CreateUpdateBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.BranchName, form.NewBranchName, "")
198198
} else {
199-
err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName)
199+
err = repo_service.CreateUpdateBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.CommitID, form.NewBranchName, "")
200200
}
201201
if err != nil {
202202
if release_service.IsErrProtectedTagName(err) {

services/repository/branch.go

Lines changed: 31 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,14 @@ import (
3737
"xorm.io/builder"
3838
)
3939

40-
// CreateNewBranch creates a new repository branch
41-
func CreateNewBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName string) (err error) {
40+
// CreateUpdateBranch creates a new repository branch
41+
func CreateUpdateBranch(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, oldBranchName, branchName, sha string) (err error) {
4242
branch, err := git_model.GetBranch(ctx, repo.ID, oldBranchName)
4343
if err != nil {
4444
return err
4545
}
4646

47-
return CreateNewBranchFromCommit(ctx, doer, repo, branch.CommitID, branchName)
47+
return CreateUpdateBranchFromCommit(ctx, doer, repo, branch.CommitID, branchName, sha)
4848
}
4949

5050
// Branch contains the branch information
@@ -255,6 +255,22 @@ func loadOneBranch(ctx context.Context, repo *repo_model.Repository, dbBranch *g
255255
}, nil
256256
}
257257

258+
func checkBranchSha(ctx context.Context, repo *repo_model.Repository, name, sha string) error {
259+
branch, err := git_model.GetBranch(ctx, repo.ID, name)
260+
if err != nil {
261+
return err
262+
}
263+
264+
if branch.CommitID != sha {
265+
return git_model.ErrBranchShaMismatch{
266+
BranchName: name,
267+
Expected: sha,
268+
Actual: branch.CommitID,
269+
}
270+
}
271+
return nil
272+
}
273+
258274
// checkBranchName validates branch name with existing repository branches
259275
func checkBranchName(ctx context.Context, repo *repo_model.Repository, name string) error {
260276
_, err := gitrepo.WalkReferences(ctx, repo, func(_, refName string) error {
@@ -373,20 +389,27 @@ func SyncBranchesToDB(ctx context.Context, repoID, pusherID int64, branchNames,
373389
})
374390
}
375391

376-
// CreateNewBranchFromCommit creates a new repository branch
377-
func CreateNewBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitID, branchName string) (err error) {
392+
// CreateUpdateBranchFromCommit creates a new repository branch
393+
func CreateUpdateBranchFromCommit(ctx context.Context, doer *user_model.User, repo *repo_model.Repository, commitID, branchName, sha string) (err error) {
378394
err = repo.MustNotBeArchived()
379395
if err != nil {
380396
return err
381397
}
382398

383-
// Check if branch name can be used
384-
if err := checkBranchName(ctx, repo, branchName); err != nil {
385-
return err
399+
if sha != "" {
400+
if err := checkBranchSha(ctx, repo, branchName, sha); err != nil {
401+
return err
402+
}
403+
} else {
404+
// Check if branch name can be used
405+
if err := checkBranchName(ctx, repo, branchName); err != nil {
406+
return err
407+
}
386408
}
387409

388410
if err := git.Push(ctx, repo.RepoPath(), git.PushOptions{
389411
Remote: repo.RepoPath(),
412+
Force: sha != "",
390413
Branch: fmt.Sprintf("%s:%s%s", commitID, git.BranchPrefix, branchName),
391414
Env: repo_module.PushingEnvironment(doer, repo),
392415
}); err != nil {

templates/swagger/v1_json.tmpl

Lines changed: 86 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

0 commit comments

Comments
 (0)