@@ -483,8 +483,107 @@ func RenameBranch(ctx context.Context, repo *repo_model.Repository, doer *user_m
483483 return "" , nil
484484}
485485
486+ // UpdateBranch moves a branch reference to the provided commit.
487+ func UpdateBranch (ctx context.Context , repo * repo_model.Repository , doer * user_model.User , branchName , newCommitID , expectedOldCommitID string , force bool ) error {
488+ if err := repo .MustNotBeArchived (); err != nil {
489+ return err
490+ }
491+
492+ perm , err := access_model .GetUserRepoPermission (ctx , repo , doer )
493+ if err != nil {
494+ return err
495+ }
496+ if ! perm .CanWrite (unit .TypeCode ) {
497+ return repo_model.ErrUserDoesNotHaveAccessToRepo {
498+ UserID : doer .ID ,
499+ RepoName : repo .LowerName ,
500+ }
501+ }
502+
503+ gitRepo , err := gitrepo .OpenRepository (ctx , repo )
504+ if err != nil {
505+ return fmt .Errorf ("OpenRepository: %w" , err )
506+ }
507+ defer gitRepo .Close ()
508+
509+ branchCommit , err := gitRepo .GetBranchCommit (branchName )
510+ if err != nil {
511+ if git .IsErrNotExist (err ) {
512+ return git_model.ErrBranchNotExist {RepoID : repo .ID , BranchName : branchName }
513+ }
514+ return err
515+ }
516+ currentCommitID := branchCommit .ID .String ()
517+
518+ if expectedOldCommitID != "" {
519+ expectedID , err := gitRepo .ConvertToGitID (expectedOldCommitID )
520+ if err != nil {
521+ return fmt .Errorf ("ConvertToGitID(old): %w" , err )
522+ }
523+ if expectedID .String () != currentCommitID {
524+ return ErrBranchCommitDoesNotMatch {Expected : currentCommitID , Given : expectedID .String ()}
525+ }
526+ }
527+
528+ newID , err := gitRepo .ConvertToGitID (newCommitID )
529+ if err != nil {
530+ return fmt .Errorf ("ConvertToGitID(new): %w" , err )
531+ }
532+ newCommit , err := gitRepo .GetCommit (newID .String ())
533+ if err != nil {
534+ return err
535+ }
536+
537+ if newCommit .ID .String () == currentCommitID {
538+ return nil
539+ }
540+
541+ isForcePush , err := newCommit .IsForcePush (currentCommitID )
542+ if err != nil {
543+ return err
544+ }
545+ if isForcePush && ! force {
546+ return & git.ErrPushOutOfDate {Err : errors .New ("non fast-forward update requires force" ), StdErr : "non-fast-forward" , StdOut : "" }
547+ }
548+
549+ pushOpts := git.PushOptions {
550+ Remote : repo .RepoPath (),
551+ Branch : fmt .Sprintf ("%s:%s%s" , newCommit .ID .String (), git .BranchPrefix , branchName ),
552+ Env : repo_module .PushingEnvironment (doer , repo ),
553+ }
554+
555+ if expectedOldCommitID != "" {
556+ pushOpts .ForceWithLease = fmt .Sprintf ("%s:%s" , git .BranchPrefix + branchName , currentCommitID )
557+ }
558+ if isForcePush || force {
559+ pushOpts .Force = true
560+ }
561+
562+ if err := git .Push (ctx , repo .RepoPath (), pushOpts ); err != nil {
563+ return err
564+ }
565+
566+ return nil
567+ }
568+
486569var ErrBranchIsDefault = util .ErrorWrap (util .ErrPermissionDenied , "branch is default" )
487570
571+ // ErrBranchCommitDoesNotMatch indicates the provided old commit id does not match the branch tip.
572+ type ErrBranchCommitDoesNotMatch struct {
573+ Expected string
574+ Given string
575+ }
576+
577+ // IsErrBranchCommitDoesNotMatch checks if the error is ErrBranchCommitDoesNotMatch.
578+ func IsErrBranchCommitDoesNotMatch (err error ) bool {
579+ _ , ok := err .(ErrBranchCommitDoesNotMatch )
580+ return ok
581+ }
582+
583+ func (e ErrBranchCommitDoesNotMatch ) Error () string {
584+ return fmt .Sprintf ("branch commit does not match [expected: %s, given: %s]" , e .Expected , e .Given )
585+ }
586+
488587func CanDeleteBranch (ctx context.Context , repo * repo_model.Repository , branchName string , doer * user_model.User ) error {
489588 if branchName == repo .DefaultBranch {
490589 return ErrBranchIsDefault
0 commit comments