Skip to content

Commit a440116

Browse files
authored
Support updating branch via API (#35951)
Resolve #35368
1 parent 24b81ac commit a440116

File tree

8 files changed

+321
-7
lines changed

8 files changed

+321
-7
lines changed

modules/git/repo.go

Lines changed: 10 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -186,18 +186,21 @@ func Clone(ctx context.Context, from, to string, opts CloneRepoOptions) error {
186186

187187
// PushOptions options when push to remote
188188
type PushOptions struct {
189-
Remote string
190-
Branch string
191-
Force bool
192-
Mirror bool
193-
Env []string
194-
Timeout time.Duration
189+
Remote string
190+
Branch string
191+
Force bool
192+
ForceWithLease string
193+
Mirror bool
194+
Env []string
195+
Timeout time.Duration
195196
}
196197

197198
// Push pushs local commits to given remote branch.
198199
func Push(ctx context.Context, repoPath string, opts PushOptions) error {
199200
cmd := gitcmd.NewCommand("push")
200-
if opts.Force {
201+
if opts.ForceWithLease != "" {
202+
cmd.AddOptionFormat("--force-with-lease=%s", opts.ForceWithLease)
203+
} else if opts.Force {
201204
cmd.AddArguments("-f")
202205
}
203206
if opts.Mirror {

modules/structs/repo.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -292,6 +292,21 @@ type RenameBranchRepoOption struct {
292292
Name string `json:"name" binding:"Required;GitRefName;MaxSize(100)"`
293293
}
294294

295+
// UpdateBranchRepoOption options when updating a branch reference in a repository
296+
// swagger:model
297+
type UpdateBranchRepoOption struct {
298+
// New commit SHA (or any ref) the branch should point to
299+
//
300+
// required: true
301+
NewCommitID string `json:"new_commit_id" binding:"Required"`
302+
303+
// Expected old commit SHA of the branch; if provided it must match the current tip
304+
OldCommitID string `json:"old_commit_id"`
305+
306+
// Force update even if the change is not a fast-forward
307+
Force bool `json:"force"`
308+
}
309+
295310
// TransferRepoOption options when transfer a repository's ownership
296311
// swagger:model
297312
type TransferRepoOption 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: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -380,6 +380,81 @@ func ListBranches(ctx *context.APIContext) {
380380
ctx.JSON(http.StatusOK, apiBranches)
381381
}
382382

383+
// UpdateBranch moves a branch reference to a new commit.
384+
func UpdateBranch(ctx *context.APIContext) {
385+
// swagger:operation PUT /repos/{owner}/{repo}/branches/{branch} repository repoUpdateBranch
386+
// ---
387+
// summary: Update a branch reference to a new commit
388+
// consumes:
389+
// - application/json
390+
// produces:
391+
// - application/json
392+
// parameters:
393+
// - name: owner
394+
// in: path
395+
// description: owner of the repo
396+
// type: string
397+
// required: true
398+
// - name: repo
399+
// in: path
400+
// description: name of the repo
401+
// type: string
402+
// required: true
403+
// - name: branch
404+
// in: path
405+
// description: name of the branch
406+
// type: string
407+
// required: true
408+
// - name: body
409+
// in: body
410+
// schema:
411+
// "$ref": "#/definitions/UpdateBranchRepoOption"
412+
// responses:
413+
// "204":
414+
// "$ref": "#/responses/empty"
415+
// "403":
416+
// "$ref": "#/responses/forbidden"
417+
// "404":
418+
// "$ref": "#/responses/notFound"
419+
// "409":
420+
// "$ref": "#/responses/conflict"
421+
// "422":
422+
// "$ref": "#/responses/validationError"
423+
424+
opt := web.GetForm(ctx).(*api.UpdateBranchRepoOption)
425+
426+
branchName := ctx.PathParam("*")
427+
repo := ctx.Repo.Repository
428+
429+
if repo.IsEmpty {
430+
ctx.APIError(http.StatusNotFound, "Git Repository is empty.")
431+
return
432+
}
433+
434+
if repo.IsMirror {
435+
ctx.APIError(http.StatusForbidden, "Git Repository is a mirror.")
436+
return
437+
}
438+
439+
// permission check has been done in api.go
440+
if err := repo_service.UpdateBranch(ctx, repo, ctx.Repo.GitRepo, ctx.Doer, branchName, opt.NewCommitID, opt.OldCommitID, opt.Force); err != nil {
441+
switch {
442+
case git_model.IsErrBranchNotExist(err):
443+
ctx.APIErrorNotFound(err)
444+
case errors.Is(err, util.ErrInvalidArgument):
445+
ctx.APIError(http.StatusUnprocessableEntity, err)
446+
case git.IsErrPushRejected(err):
447+
rej := err.(*git.ErrPushRejected)
448+
ctx.APIError(http.StatusForbidden, rej.Message)
449+
default:
450+
ctx.APIErrorInternal(err)
451+
}
452+
return
453+
}
454+
455+
ctx.Status(http.StatusNoContent)
456+
}
457+
383458
// RenameBranch renames a repository's branch.
384459
func RenameBranch(ctx *context.APIContext) {
385460
// swagger:operation PATCH /repos/{owner}/{repo}/branches/{branch} repository repoRenameBranch

routers/api/v1/swagger/options.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -147,6 +147,8 @@ type swaggerParameterBodies struct {
147147

148148
// in:body
149149
CreateBranchRepoOption api.CreateBranchRepoOption
150+
// in:body
151+
UpdateBranchRepoOption api.UpdateBranchRepoOption
150152

151153
// in:body
152154
CreateBranchProtectionOption api.CreateBranchProtectionOption

services/repository/branch.go

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -482,6 +482,64 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
482482
return "", nil
483483
}
484484

485+
// UpdateBranch moves a branch reference to the provided commit. permission check should be done before calling this function.
486+
func UpdateBranch(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, doer *user_model.User, branchName, newCommitID, expectedOldCommitID string, force bool) error {
487+
branch, err := git_model.GetBranch(ctx, repo.ID, branchName)
488+
if err != nil {
489+
return err
490+
}
491+
if branch.IsDeleted {
492+
return git_model.ErrBranchNotExist{
493+
BranchName: branchName,
494+
}
495+
}
496+
497+
if expectedOldCommitID != "" {
498+
expectedID, err := gitRepo.ConvertToGitID(expectedOldCommitID)
499+
if err != nil {
500+
return fmt.Errorf("ConvertToGitID(old): %w", err)
501+
}
502+
if expectedID.String() != branch.CommitID {
503+
return util.NewInvalidArgumentErrorf("branch commit does not match [expected: %s, given: %s]", expectedID.String(), branch.CommitID)
504+
}
505+
}
506+
507+
newID, err := gitRepo.ConvertToGitID(newCommitID)
508+
if err != nil {
509+
return fmt.Errorf("ConvertToGitID(new): %w", err)
510+
}
511+
newCommit, err := gitRepo.GetCommit(newID.String())
512+
if err != nil {
513+
return err
514+
}
515+
516+
if newCommit.ID.String() == branch.CommitID {
517+
return nil
518+
}
519+
520+
isForcePush, err := newCommit.IsForcePush(branch.CommitID)
521+
if err != nil {
522+
return err
523+
}
524+
if isForcePush && !force {
525+
return util.NewInvalidArgumentErrorf("Force push %s need a confirm force parameter", branchName)
526+
}
527+
528+
pushOpts := git.PushOptions{
529+
Remote: repo.RepoPath(),
530+
Branch: fmt.Sprintf("%s:%s%s", newCommit.ID.String(), git.BranchPrefix, branchName),
531+
Env: repo_module.PushingEnvironment(doer, repo),
532+
Force: isForcePush || force,
533+
}
534+
535+
if expectedOldCommitID != "" {
536+
pushOpts.ForceWithLease = fmt.Sprintf("%s:%s", git.BranchPrefix+branchName, branch.CommitID)
537+
}
538+
539+
// branch protection will be checked in the pre received hook, so that we don't need any check here
540+
return gitrepo.Push(ctx, repo, repo, pushOpts)
541+
}
542+
485543
var ErrBranchIsDefault = util.ErrorWrap(util.ErrPermissionDenied, "branch is default")
486544

487545
func CanDeleteBranch(ctx context.Context, repo *repo_model.Repository, branchName string, doer *user_model.User) error {

templates/swagger/v1_json.tmpl

Lines changed: 85 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)