code.gitea.io/gitea@v1.21.7/routers/api/v1/repo/branch.go (about)

     1  // Copyright 2016 The Gogs Authors. All rights reserved.
     2  // Copyright 2019 The Gitea Authors. All rights reserved.
     3  // SPDX-License-Identifier: MIT
     4  
     5  package repo
     6  
     7  import (
     8  	"errors"
     9  	"fmt"
    10  	"net/http"
    11  
    12  	"code.gitea.io/gitea/models"
    13  	git_model "code.gitea.io/gitea/models/git"
    14  	"code.gitea.io/gitea/models/organization"
    15  	user_model "code.gitea.io/gitea/models/user"
    16  	"code.gitea.io/gitea/modules/context"
    17  	"code.gitea.io/gitea/modules/git"
    18  	repo_module "code.gitea.io/gitea/modules/repository"
    19  	api "code.gitea.io/gitea/modules/structs"
    20  	"code.gitea.io/gitea/modules/util"
    21  	"code.gitea.io/gitea/modules/web"
    22  	"code.gitea.io/gitea/routers/api/v1/utils"
    23  	"code.gitea.io/gitea/services/convert"
    24  	pull_service "code.gitea.io/gitea/services/pull"
    25  	repo_service "code.gitea.io/gitea/services/repository"
    26  )
    27  
    28  // GetBranch get a branch of a repository
    29  func GetBranch(ctx *context.APIContext) {
    30  	// swagger:operation GET /repos/{owner}/{repo}/branches/{branch} repository repoGetBranch
    31  	// ---
    32  	// summary: Retrieve a specific branch from a repository, including its effective branch protection
    33  	// produces:
    34  	// - application/json
    35  	// parameters:
    36  	// - name: owner
    37  	//   in: path
    38  	//   description: owner of the repo
    39  	//   type: string
    40  	//   required: true
    41  	// - name: repo
    42  	//   in: path
    43  	//   description: name of the repo
    44  	//   type: string
    45  	//   required: true
    46  	// - name: branch
    47  	//   in: path
    48  	//   description: branch to get
    49  	//   type: string
    50  	//   required: true
    51  	// responses:
    52  	//   "200":
    53  	//     "$ref": "#/responses/Branch"
    54  	//   "404":
    55  	//     "$ref": "#/responses/notFound"
    56  
    57  	branchName := ctx.Params("*")
    58  
    59  	branch, err := ctx.Repo.GitRepo.GetBranch(branchName)
    60  	if err != nil {
    61  		if git.IsErrBranchNotExist(err) {
    62  			ctx.NotFound(err)
    63  		} else {
    64  			ctx.Error(http.StatusInternalServerError, "GetBranch", err)
    65  		}
    66  		return
    67  	}
    68  
    69  	c, err := branch.GetCommit()
    70  	if err != nil {
    71  		ctx.Error(http.StatusInternalServerError, "GetCommit", err)
    72  		return
    73  	}
    74  
    75  	branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branchName)
    76  	if err != nil {
    77  		ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
    78  		return
    79  	}
    80  
    81  	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
    82  	if err != nil {
    83  		ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
    84  		return
    85  	}
    86  
    87  	ctx.JSON(http.StatusOK, br)
    88  }
    89  
    90  // DeleteBranch get a branch of a repository
    91  func DeleteBranch(ctx *context.APIContext) {
    92  	// swagger:operation DELETE /repos/{owner}/{repo}/branches/{branch} repository repoDeleteBranch
    93  	// ---
    94  	// summary: Delete a specific branch from a repository
    95  	// produces:
    96  	// - application/json
    97  	// parameters:
    98  	// - name: owner
    99  	//   in: path
   100  	//   description: owner of the repo
   101  	//   type: string
   102  	//   required: true
   103  	// - name: repo
   104  	//   in: path
   105  	//   description: name of the repo
   106  	//   type: string
   107  	//   required: true
   108  	// - name: branch
   109  	//   in: path
   110  	//   description: branch to delete
   111  	//   type: string
   112  	//   required: true
   113  	// responses:
   114  	//   "204":
   115  	//     "$ref": "#/responses/empty"
   116  	//   "403":
   117  	//     "$ref": "#/responses/error"
   118  	//   "404":
   119  	//     "$ref": "#/responses/notFound"
   120  
   121  	if ctx.Repo.Repository.IsEmpty {
   122  		ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
   123  		return
   124  	}
   125  
   126  	if ctx.Repo.Repository.IsArchived {
   127  		ctx.Error(http.StatusForbidden, "", "Git Repository is archived.")
   128  		return
   129  	}
   130  
   131  	if ctx.Repo.Repository.IsMirror {
   132  		ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.")
   133  		return
   134  	}
   135  
   136  	branchName := ctx.Params("*")
   137  
   138  	if ctx.Repo.Repository.IsEmpty {
   139  		ctx.Error(http.StatusForbidden, "", "Git Repository is empty.")
   140  		return
   141  	}
   142  
   143  	// check whether branches of this repository has been synced
   144  	totalNumOfBranches, err := git_model.CountBranches(ctx, git_model.FindBranchOptions{
   145  		RepoID:          ctx.Repo.Repository.ID,
   146  		IsDeletedBranch: util.OptionalBoolFalse,
   147  	})
   148  	if err != nil {
   149  		ctx.Error(http.StatusInternalServerError, "CountBranches", err)
   150  		return
   151  	}
   152  	if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch
   153  		_, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
   154  		if err != nil {
   155  			ctx.ServerError("SyncRepoBranches", err)
   156  			return
   157  		}
   158  	}
   159  
   160  	if ctx.Repo.Repository.IsArchived {
   161  		ctx.Error(http.StatusForbidden, "IsArchived", fmt.Errorf("can not delete branch of an archived repository"))
   162  		return
   163  	}
   164  	if ctx.Repo.Repository.IsMirror {
   165  		ctx.Error(http.StatusForbidden, "IsMirrored", fmt.Errorf("can not delete branch of an mirror repository"))
   166  		return
   167  	}
   168  
   169  	if err := repo_service.DeleteBranch(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, branchName); err != nil {
   170  		switch {
   171  		case git.IsErrBranchNotExist(err):
   172  			ctx.NotFound(err)
   173  		case errors.Is(err, repo_service.ErrBranchIsDefault):
   174  			ctx.Error(http.StatusForbidden, "DefaultBranch", fmt.Errorf("can not delete default branch"))
   175  		case errors.Is(err, git_model.ErrBranchIsProtected):
   176  			ctx.Error(http.StatusForbidden, "IsProtectedBranch", fmt.Errorf("branch protected"))
   177  		default:
   178  			ctx.Error(http.StatusInternalServerError, "DeleteBranch", err)
   179  		}
   180  		return
   181  	}
   182  
   183  	ctx.Status(http.StatusNoContent)
   184  }
   185  
   186  // CreateBranch creates a branch for a user's repository
   187  func CreateBranch(ctx *context.APIContext) {
   188  	// swagger:operation POST /repos/{owner}/{repo}/branches repository repoCreateBranch
   189  	// ---
   190  	// summary: Create a branch
   191  	// consumes:
   192  	// - application/json
   193  	// produces:
   194  	// - application/json
   195  	// parameters:
   196  	// - name: owner
   197  	//   in: path
   198  	//   description: owner of the repo
   199  	//   type: string
   200  	//   required: true
   201  	// - name: repo
   202  	//   in: path
   203  	//   description: name of the repo
   204  	//   type: string
   205  	//   required: true
   206  	// - name: body
   207  	//   in: body
   208  	//   schema:
   209  	//     "$ref": "#/definitions/CreateBranchRepoOption"
   210  	// responses:
   211  	//   "201":
   212  	//     "$ref": "#/responses/Branch"
   213  	//   "403":
   214  	//     description: The branch is archived or a mirror.
   215  	//   "404":
   216  	//     description: The old branch does not exist.
   217  	//   "409":
   218  	//     description: The branch with the same name already exists.
   219  
   220  	if ctx.Repo.Repository.IsEmpty {
   221  		ctx.Error(http.StatusNotFound, "", "Git Repository is empty.")
   222  		return
   223  	}
   224  
   225  	if ctx.Repo.Repository.IsArchived {
   226  		ctx.Error(http.StatusForbidden, "", "Git Repository is archived.")
   227  		return
   228  	}
   229  
   230  	if ctx.Repo.Repository.IsMirror {
   231  		ctx.Error(http.StatusForbidden, "", "Git Repository is a mirror.")
   232  		return
   233  	}
   234  
   235  	opt := web.GetForm(ctx).(*api.CreateBranchRepoOption)
   236  
   237  	var oldCommit *git.Commit
   238  	var err error
   239  
   240  	if len(opt.OldRefName) > 0 {
   241  		oldCommit, err = ctx.Repo.GitRepo.GetCommit(opt.OldRefName)
   242  		if err != nil {
   243  			ctx.Error(http.StatusInternalServerError, "GetCommit", err)
   244  			return
   245  		}
   246  	} else if len(opt.OldBranchName) > 0 { //nolint
   247  		if ctx.Repo.GitRepo.IsBranchExist(opt.OldBranchName) { //nolint
   248  			oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(opt.OldBranchName) //nolint
   249  			if err != nil {
   250  				ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
   251  				return
   252  			}
   253  		} else {
   254  			ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
   255  			return
   256  		}
   257  	} else {
   258  		oldCommit, err = ctx.Repo.GitRepo.GetBranchCommit(ctx.Repo.Repository.DefaultBranch)
   259  		if err != nil {
   260  			ctx.Error(http.StatusInternalServerError, "GetBranchCommit", err)
   261  			return
   262  		}
   263  	}
   264  
   265  	err = repo_service.CreateNewBranchFromCommit(ctx, ctx.Doer, ctx.Repo.Repository, ctx.Repo.GitRepo, oldCommit.ID.String(), opt.BranchName)
   266  	if err != nil {
   267  		if git_model.IsErrBranchNotExist(err) {
   268  			ctx.Error(http.StatusNotFound, "", "The old branch does not exist")
   269  		} else if models.IsErrTagAlreadyExists(err) {
   270  			ctx.Error(http.StatusConflict, "", "The branch with the same tag already exists.")
   271  		} else if git_model.IsErrBranchAlreadyExists(err) || git.IsErrPushOutOfDate(err) {
   272  			ctx.Error(http.StatusConflict, "", "The branch already exists.")
   273  		} else if git_model.IsErrBranchNameConflict(err) {
   274  			ctx.Error(http.StatusConflict, "", "The branch with the same name already exists.")
   275  		} else {
   276  			ctx.Error(http.StatusInternalServerError, "CreateNewBranchFromCommit", err)
   277  		}
   278  		return
   279  	}
   280  
   281  	branch, err := ctx.Repo.GitRepo.GetBranch(opt.BranchName)
   282  	if err != nil {
   283  		ctx.Error(http.StatusInternalServerError, "GetBranch", err)
   284  		return
   285  	}
   286  
   287  	commit, err := branch.GetCommit()
   288  	if err != nil {
   289  		ctx.Error(http.StatusInternalServerError, "GetCommit", err)
   290  		return
   291  	}
   292  
   293  	branchProtection, err := git_model.GetFirstMatchProtectedBranchRule(ctx, ctx.Repo.Repository.ID, branch.Name)
   294  	if err != nil {
   295  		ctx.Error(http.StatusInternalServerError, "GetBranchProtection", err)
   296  		return
   297  	}
   298  
   299  	br, err := convert.ToBranch(ctx, ctx.Repo.Repository, branch.Name, commit, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
   300  	if err != nil {
   301  		ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
   302  		return
   303  	}
   304  
   305  	ctx.JSON(http.StatusCreated, br)
   306  }
   307  
   308  // ListBranches list all the branches of a repository
   309  func ListBranches(ctx *context.APIContext) {
   310  	// swagger:operation GET /repos/{owner}/{repo}/branches repository repoListBranches
   311  	// ---
   312  	// summary: List a repository's branches
   313  	// produces:
   314  	// - application/json
   315  	// parameters:
   316  	// - name: owner
   317  	//   in: path
   318  	//   description: owner of the repo
   319  	//   type: string
   320  	//   required: true
   321  	// - name: repo
   322  	//   in: path
   323  	//   description: name of the repo
   324  	//   type: string
   325  	//   required: true
   326  	// - name: page
   327  	//   in: query
   328  	//   description: page number of results to return (1-based)
   329  	//   type: integer
   330  	// - name: limit
   331  	//   in: query
   332  	//   description: page size of results
   333  	//   type: integer
   334  	// responses:
   335  	//   "200":
   336  	//     "$ref": "#/responses/BranchList"
   337  
   338  	var totalNumOfBranches int64
   339  	var apiBranches []*api.Branch
   340  
   341  	listOptions := utils.GetListOptions(ctx)
   342  
   343  	if !ctx.Repo.Repository.IsEmpty {
   344  		if ctx.Repo.GitRepo == nil {
   345  			ctx.Error(http.StatusInternalServerError, "Load git repository failed", nil)
   346  			return
   347  		}
   348  
   349  		branchOpts := git_model.FindBranchOptions{
   350  			ListOptions:     listOptions,
   351  			RepoID:          ctx.Repo.Repository.ID,
   352  			IsDeletedBranch: util.OptionalBoolFalse,
   353  		}
   354  		var err error
   355  		totalNumOfBranches, err = git_model.CountBranches(ctx, branchOpts)
   356  		if err != nil {
   357  			ctx.Error(http.StatusInternalServerError, "CountBranches", err)
   358  			return
   359  		}
   360  		if totalNumOfBranches == 0 { // sync branches immediately because non-empty repository should have at least 1 branch
   361  			totalNumOfBranches, err = repo_module.SyncRepoBranches(ctx, ctx.Repo.Repository.ID, 0)
   362  			if err != nil {
   363  				ctx.ServerError("SyncRepoBranches", err)
   364  				return
   365  			}
   366  		}
   367  
   368  		rules, err := git_model.FindRepoProtectedBranchRules(ctx, ctx.Repo.Repository.ID)
   369  		if err != nil {
   370  			ctx.Error(http.StatusInternalServerError, "FindMatchedProtectedBranchRules", err)
   371  			return
   372  		}
   373  
   374  		branches, err := git_model.FindBranches(ctx, branchOpts)
   375  		if err != nil {
   376  			ctx.Error(http.StatusInternalServerError, "GetBranches", err)
   377  			return
   378  		}
   379  
   380  		apiBranches = make([]*api.Branch, 0, len(branches))
   381  		for i := range branches {
   382  			c, err := ctx.Repo.GitRepo.GetBranchCommit(branches[i].Name)
   383  			if err != nil {
   384  				// Skip if this branch doesn't exist anymore.
   385  				if git.IsErrNotExist(err) {
   386  					totalNumOfBranches--
   387  					continue
   388  				}
   389  				ctx.Error(http.StatusInternalServerError, "GetCommit", err)
   390  				return
   391  			}
   392  
   393  			branchProtection := rules.GetFirstMatched(branches[i].Name)
   394  			apiBranch, err := convert.ToBranch(ctx, ctx.Repo.Repository, branches[i].Name, c, branchProtection, ctx.Doer, ctx.Repo.IsAdmin())
   395  			if err != nil {
   396  				ctx.Error(http.StatusInternalServerError, "convert.ToBranch", err)
   397  				return
   398  			}
   399  			apiBranches = append(apiBranches, apiBranch)
   400  		}
   401  	}
   402  
   403  	ctx.SetLinkHeader(int(totalNumOfBranches), listOptions.PageSize)
   404  	ctx.SetTotalCountHeader(totalNumOfBranches)
   405  	ctx.JSON(http.StatusOK, apiBranches)
   406  }
   407  
   408  // GetBranchProtection gets a branch protection
   409  func GetBranchProtection(ctx *context.APIContext) {
   410  	// swagger:operation GET /repos/{owner}/{repo}/branch_protections/{name} repository repoGetBranchProtection
   411  	// ---
   412  	// summary: Get a specific branch protection for the repository
   413  	// produces:
   414  	// - application/json
   415  	// parameters:
   416  	// - name: owner
   417  	//   in: path
   418  	//   description: owner of the repo
   419  	//   type: string
   420  	//   required: true
   421  	// - name: repo
   422  	//   in: path
   423  	//   description: name of the repo
   424  	//   type: string
   425  	//   required: true
   426  	// - name: name
   427  	//   in: path
   428  	//   description: name of protected branch
   429  	//   type: string
   430  	//   required: true
   431  	// responses:
   432  	//   "200":
   433  	//     "$ref": "#/responses/BranchProtection"
   434  	//   "404":
   435  	//     "$ref": "#/responses/notFound"
   436  
   437  	repo := ctx.Repo.Repository
   438  	bpName := ctx.Params(":name")
   439  	bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
   440  	if err != nil {
   441  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
   442  		return
   443  	}
   444  	if bp == nil || bp.RepoID != repo.ID {
   445  		ctx.NotFound()
   446  		return
   447  	}
   448  
   449  	ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp))
   450  }
   451  
   452  // ListBranchProtections list branch protections for a repo
   453  func ListBranchProtections(ctx *context.APIContext) {
   454  	// swagger:operation GET /repos/{owner}/{repo}/branch_protections repository repoListBranchProtection
   455  	// ---
   456  	// summary: List branch protections for a repository
   457  	// produces:
   458  	// - application/json
   459  	// parameters:
   460  	// - name: owner
   461  	//   in: path
   462  	//   description: owner of the repo
   463  	//   type: string
   464  	//   required: true
   465  	// - name: repo
   466  	//   in: path
   467  	//   description: name of the repo
   468  	//   type: string
   469  	//   required: true
   470  	// responses:
   471  	//   "200":
   472  	//     "$ref": "#/responses/BranchProtectionList"
   473  
   474  	repo := ctx.Repo.Repository
   475  	bps, err := git_model.FindRepoProtectedBranchRules(ctx, repo.ID)
   476  	if err != nil {
   477  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranches", err)
   478  		return
   479  	}
   480  	apiBps := make([]*api.BranchProtection, len(bps))
   481  	for i := range bps {
   482  		apiBps[i] = convert.ToBranchProtection(ctx, bps[i])
   483  	}
   484  
   485  	ctx.JSON(http.StatusOK, apiBps)
   486  }
   487  
   488  // CreateBranchProtection creates a branch protection for a repo
   489  func CreateBranchProtection(ctx *context.APIContext) {
   490  	// swagger:operation POST /repos/{owner}/{repo}/branch_protections repository repoCreateBranchProtection
   491  	// ---
   492  	// summary: Create a branch protections for a repository
   493  	// consumes:
   494  	// - application/json
   495  	// produces:
   496  	// - application/json
   497  	// parameters:
   498  	// - name: owner
   499  	//   in: path
   500  	//   description: owner of the repo
   501  	//   type: string
   502  	//   required: true
   503  	// - name: repo
   504  	//   in: path
   505  	//   description: name of the repo
   506  	//   type: string
   507  	//   required: true
   508  	// - name: body
   509  	//   in: body
   510  	//   schema:
   511  	//     "$ref": "#/definitions/CreateBranchProtectionOption"
   512  	// responses:
   513  	//   "201":
   514  	//     "$ref": "#/responses/BranchProtection"
   515  	//   "403":
   516  	//     "$ref": "#/responses/forbidden"
   517  	//   "404":
   518  	//     "$ref": "#/responses/notFound"
   519  	//   "422":
   520  	//     "$ref": "#/responses/validationError"
   521  
   522  	form := web.GetForm(ctx).(*api.CreateBranchProtectionOption)
   523  	repo := ctx.Repo.Repository
   524  
   525  	ruleName := form.RuleName
   526  	if ruleName == "" {
   527  		ruleName = form.BranchName //nolint
   528  	}
   529  	if len(ruleName) == 0 {
   530  		ctx.Error(http.StatusBadRequest, "both rule_name and branch_name are empty", "both rule_name and branch_name are empty")
   531  		return
   532  	}
   533  
   534  	isPlainRule := !git_model.IsRuleNameSpecial(ruleName)
   535  	var isBranchExist bool
   536  	if isPlainRule {
   537  		isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), ruleName)
   538  	}
   539  
   540  	protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, ruleName)
   541  	if err != nil {
   542  		ctx.Error(http.StatusInternalServerError, "GetProtectBranchOfRepoByName", err)
   543  		return
   544  	} else if protectBranch != nil {
   545  		ctx.Error(http.StatusForbidden, "Create branch protection", "Branch protection already exist")
   546  		return
   547  	}
   548  
   549  	var requiredApprovals int64
   550  	if form.RequiredApprovals > 0 {
   551  		requiredApprovals = form.RequiredApprovals
   552  	}
   553  
   554  	whitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
   555  	if err != nil {
   556  		if user_model.IsErrUserNotExist(err) {
   557  			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   558  			return
   559  		}
   560  		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   561  		return
   562  	}
   563  	mergeWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
   564  	if err != nil {
   565  		if user_model.IsErrUserNotExist(err) {
   566  			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   567  			return
   568  		}
   569  		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   570  		return
   571  	}
   572  	approvalsWhitelistUsers, err := user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
   573  	if err != nil {
   574  		if user_model.IsErrUserNotExist(err) {
   575  			ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   576  			return
   577  		}
   578  		ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   579  		return
   580  	}
   581  	var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
   582  	if repo.Owner.IsOrganization() {
   583  		whitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
   584  		if err != nil {
   585  			if organization.IsErrTeamNotExist(err) {
   586  				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   587  				return
   588  			}
   589  			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   590  			return
   591  		}
   592  		mergeWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
   593  		if err != nil {
   594  			if organization.IsErrTeamNotExist(err) {
   595  				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   596  				return
   597  			}
   598  			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   599  			return
   600  		}
   601  		approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
   602  		if err != nil {
   603  			if organization.IsErrTeamNotExist(err) {
   604  				ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   605  				return
   606  			}
   607  			ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   608  			return
   609  		}
   610  	}
   611  
   612  	protectBranch = &git_model.ProtectedBranch{
   613  		RepoID:                        ctx.Repo.Repository.ID,
   614  		RuleName:                      ruleName,
   615  		CanPush:                       form.EnablePush,
   616  		EnableWhitelist:               form.EnablePush && form.EnablePushWhitelist,
   617  		EnableMergeWhitelist:          form.EnableMergeWhitelist,
   618  		WhitelistDeployKeys:           form.EnablePush && form.EnablePushWhitelist && form.PushWhitelistDeployKeys,
   619  		EnableStatusCheck:             form.EnableStatusCheck,
   620  		StatusCheckContexts:           form.StatusCheckContexts,
   621  		EnableApprovalsWhitelist:      form.EnableApprovalsWhitelist,
   622  		RequiredApprovals:             requiredApprovals,
   623  		BlockOnRejectedReviews:        form.BlockOnRejectedReviews,
   624  		BlockOnOfficialReviewRequests: form.BlockOnOfficialReviewRequests,
   625  		DismissStaleApprovals:         form.DismissStaleApprovals,
   626  		RequireSignedCommits:          form.RequireSignedCommits,
   627  		ProtectedFilePatterns:         form.ProtectedFilePatterns,
   628  		UnprotectedFilePatterns:       form.UnprotectedFilePatterns,
   629  		BlockOnOutdatedBranch:         form.BlockOnOutdatedBranch,
   630  	}
   631  
   632  	err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
   633  		UserIDs:          whitelistUsers,
   634  		TeamIDs:          whitelistTeams,
   635  		MergeUserIDs:     mergeWhitelistUsers,
   636  		MergeTeamIDs:     mergeWhitelistTeams,
   637  		ApprovalsUserIDs: approvalsWhitelistUsers,
   638  		ApprovalsTeamIDs: approvalsWhitelistTeams,
   639  	})
   640  	if err != nil {
   641  		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err)
   642  		return
   643  	}
   644  
   645  	if isBranchExist {
   646  		if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, ruleName); err != nil {
   647  			ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
   648  			return
   649  		}
   650  	} else {
   651  		if !isPlainRule {
   652  			if ctx.Repo.GitRepo == nil {
   653  				ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
   654  				if err != nil {
   655  					ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
   656  					return
   657  				}
   658  				defer func() {
   659  					ctx.Repo.GitRepo.Close()
   660  					ctx.Repo.GitRepo = nil
   661  				}()
   662  			}
   663  			// FIXME: since we only need to recheck files protected rules, we could improve this
   664  			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, ruleName)
   665  			if err != nil {
   666  				ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
   667  				return
   668  			}
   669  
   670  			for _, branchName := range matchedBranches {
   671  				if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil {
   672  					ctx.Error(http.StatusInternalServerError, "CheckPRsForBaseBranch", err)
   673  					return
   674  				}
   675  			}
   676  		}
   677  	}
   678  
   679  	// Reload from db to get all whitelists
   680  	bp, err := git_model.GetProtectedBranchRuleByName(ctx, ctx.Repo.Repository.ID, ruleName)
   681  	if err != nil {
   682  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
   683  		return
   684  	}
   685  	if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
   686  		ctx.Error(http.StatusInternalServerError, "New branch protection not found", err)
   687  		return
   688  	}
   689  
   690  	ctx.JSON(http.StatusCreated, convert.ToBranchProtection(ctx, bp))
   691  }
   692  
   693  // EditBranchProtection edits a branch protection for a repo
   694  func EditBranchProtection(ctx *context.APIContext) {
   695  	// swagger:operation PATCH /repos/{owner}/{repo}/branch_protections/{name} repository repoEditBranchProtection
   696  	// ---
   697  	// summary: Edit a branch protections for a repository. Only fields that are set will be changed
   698  	// consumes:
   699  	// - application/json
   700  	// produces:
   701  	// - application/json
   702  	// parameters:
   703  	// - name: owner
   704  	//   in: path
   705  	//   description: owner of the repo
   706  	//   type: string
   707  	//   required: true
   708  	// - name: repo
   709  	//   in: path
   710  	//   description: name of the repo
   711  	//   type: string
   712  	//   required: true
   713  	// - name: name
   714  	//   in: path
   715  	//   description: name of protected branch
   716  	//   type: string
   717  	//   required: true
   718  	// - name: body
   719  	//   in: body
   720  	//   schema:
   721  	//     "$ref": "#/definitions/EditBranchProtectionOption"
   722  	// responses:
   723  	//   "200":
   724  	//     "$ref": "#/responses/BranchProtection"
   725  	//   "404":
   726  	//     "$ref": "#/responses/notFound"
   727  	//   "422":
   728  	//     "$ref": "#/responses/validationError"
   729  	form := web.GetForm(ctx).(*api.EditBranchProtectionOption)
   730  	repo := ctx.Repo.Repository
   731  	bpName := ctx.Params(":name")
   732  	protectBranch, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
   733  	if err != nil {
   734  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
   735  		return
   736  	}
   737  	if protectBranch == nil || protectBranch.RepoID != repo.ID {
   738  		ctx.NotFound()
   739  		return
   740  	}
   741  
   742  	if form.EnablePush != nil {
   743  		if !*form.EnablePush {
   744  			protectBranch.CanPush = false
   745  			protectBranch.EnableWhitelist = false
   746  			protectBranch.WhitelistDeployKeys = false
   747  		} else {
   748  			protectBranch.CanPush = true
   749  			if form.EnablePushWhitelist != nil {
   750  				if !*form.EnablePushWhitelist {
   751  					protectBranch.EnableWhitelist = false
   752  					protectBranch.WhitelistDeployKeys = false
   753  				} else {
   754  					protectBranch.EnableWhitelist = true
   755  					if form.PushWhitelistDeployKeys != nil {
   756  						protectBranch.WhitelistDeployKeys = *form.PushWhitelistDeployKeys
   757  					}
   758  				}
   759  			}
   760  		}
   761  	}
   762  
   763  	if form.EnableMergeWhitelist != nil {
   764  		protectBranch.EnableMergeWhitelist = *form.EnableMergeWhitelist
   765  	}
   766  
   767  	if form.EnableStatusCheck != nil {
   768  		protectBranch.EnableStatusCheck = *form.EnableStatusCheck
   769  	}
   770  
   771  	if form.StatusCheckContexts != nil {
   772  		protectBranch.StatusCheckContexts = form.StatusCheckContexts
   773  	}
   774  
   775  	if form.RequiredApprovals != nil && *form.RequiredApprovals >= 0 {
   776  		protectBranch.RequiredApprovals = *form.RequiredApprovals
   777  	}
   778  
   779  	if form.EnableApprovalsWhitelist != nil {
   780  		protectBranch.EnableApprovalsWhitelist = *form.EnableApprovalsWhitelist
   781  	}
   782  
   783  	if form.BlockOnRejectedReviews != nil {
   784  		protectBranch.BlockOnRejectedReviews = *form.BlockOnRejectedReviews
   785  	}
   786  
   787  	if form.BlockOnOfficialReviewRequests != nil {
   788  		protectBranch.BlockOnOfficialReviewRequests = *form.BlockOnOfficialReviewRequests
   789  	}
   790  
   791  	if form.DismissStaleApprovals != nil {
   792  		protectBranch.DismissStaleApprovals = *form.DismissStaleApprovals
   793  	}
   794  
   795  	if form.RequireSignedCommits != nil {
   796  		protectBranch.RequireSignedCommits = *form.RequireSignedCommits
   797  	}
   798  
   799  	if form.ProtectedFilePatterns != nil {
   800  		protectBranch.ProtectedFilePatterns = *form.ProtectedFilePatterns
   801  	}
   802  
   803  	if form.UnprotectedFilePatterns != nil {
   804  		protectBranch.UnprotectedFilePatterns = *form.UnprotectedFilePatterns
   805  	}
   806  
   807  	if form.BlockOnOutdatedBranch != nil {
   808  		protectBranch.BlockOnOutdatedBranch = *form.BlockOnOutdatedBranch
   809  	}
   810  
   811  	var whitelistUsers []int64
   812  	if form.PushWhitelistUsernames != nil {
   813  		whitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.PushWhitelistUsernames, false)
   814  		if err != nil {
   815  			if user_model.IsErrUserNotExist(err) {
   816  				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   817  				return
   818  			}
   819  			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   820  			return
   821  		}
   822  	} else {
   823  		whitelistUsers = protectBranch.WhitelistUserIDs
   824  	}
   825  	var mergeWhitelistUsers []int64
   826  	if form.MergeWhitelistUsernames != nil {
   827  		mergeWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.MergeWhitelistUsernames, false)
   828  		if err != nil {
   829  			if user_model.IsErrUserNotExist(err) {
   830  				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   831  				return
   832  			}
   833  			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   834  			return
   835  		}
   836  	} else {
   837  		mergeWhitelistUsers = protectBranch.MergeWhitelistUserIDs
   838  	}
   839  	var approvalsWhitelistUsers []int64
   840  	if form.ApprovalsWhitelistUsernames != nil {
   841  		approvalsWhitelistUsers, err = user_model.GetUserIDsByNames(ctx, form.ApprovalsWhitelistUsernames, false)
   842  		if err != nil {
   843  			if user_model.IsErrUserNotExist(err) {
   844  				ctx.Error(http.StatusUnprocessableEntity, "User does not exist", err)
   845  				return
   846  			}
   847  			ctx.Error(http.StatusInternalServerError, "GetUserIDsByNames", err)
   848  			return
   849  		}
   850  	} else {
   851  		approvalsWhitelistUsers = protectBranch.ApprovalsWhitelistUserIDs
   852  	}
   853  
   854  	var whitelistTeams, mergeWhitelistTeams, approvalsWhitelistTeams []int64
   855  	if repo.Owner.IsOrganization() {
   856  		if form.PushWhitelistTeams != nil {
   857  			whitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.PushWhitelistTeams, false)
   858  			if err != nil {
   859  				if organization.IsErrTeamNotExist(err) {
   860  					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   861  					return
   862  				}
   863  				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   864  				return
   865  			}
   866  		} else {
   867  			whitelistTeams = protectBranch.WhitelistTeamIDs
   868  		}
   869  		if form.MergeWhitelistTeams != nil {
   870  			mergeWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.MergeWhitelistTeams, false)
   871  			if err != nil {
   872  				if organization.IsErrTeamNotExist(err) {
   873  					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   874  					return
   875  				}
   876  				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   877  				return
   878  			}
   879  		} else {
   880  			mergeWhitelistTeams = protectBranch.MergeWhitelistTeamIDs
   881  		}
   882  		if form.ApprovalsWhitelistTeams != nil {
   883  			approvalsWhitelistTeams, err = organization.GetTeamIDsByNames(repo.OwnerID, form.ApprovalsWhitelistTeams, false)
   884  			if err != nil {
   885  				if organization.IsErrTeamNotExist(err) {
   886  					ctx.Error(http.StatusUnprocessableEntity, "Team does not exist", err)
   887  					return
   888  				}
   889  				ctx.Error(http.StatusInternalServerError, "GetTeamIDsByNames", err)
   890  				return
   891  			}
   892  		} else {
   893  			approvalsWhitelistTeams = protectBranch.ApprovalsWhitelistTeamIDs
   894  		}
   895  	}
   896  
   897  	err = git_model.UpdateProtectBranch(ctx, ctx.Repo.Repository, protectBranch, git_model.WhitelistOptions{
   898  		UserIDs:          whitelistUsers,
   899  		TeamIDs:          whitelistTeams,
   900  		MergeUserIDs:     mergeWhitelistUsers,
   901  		MergeTeamIDs:     mergeWhitelistTeams,
   902  		ApprovalsUserIDs: approvalsWhitelistUsers,
   903  		ApprovalsTeamIDs: approvalsWhitelistTeams,
   904  	})
   905  	if err != nil {
   906  		ctx.Error(http.StatusInternalServerError, "UpdateProtectBranch", err)
   907  		return
   908  	}
   909  
   910  	isPlainRule := !git_model.IsRuleNameSpecial(bpName)
   911  	var isBranchExist bool
   912  	if isPlainRule {
   913  		isBranchExist = git.IsBranchExist(ctx.Req.Context(), ctx.Repo.Repository.RepoPath(), bpName)
   914  	}
   915  
   916  	if isBranchExist {
   917  		if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, bpName); err != nil {
   918  			ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
   919  			return
   920  		}
   921  	} else {
   922  		if !isPlainRule {
   923  			if ctx.Repo.GitRepo == nil {
   924  				ctx.Repo.GitRepo, err = git.OpenRepository(ctx, ctx.Repo.Repository.RepoPath())
   925  				if err != nil {
   926  					ctx.Error(http.StatusInternalServerError, "OpenRepository", err)
   927  					return
   928  				}
   929  				defer func() {
   930  					ctx.Repo.GitRepo.Close()
   931  					ctx.Repo.GitRepo = nil
   932  				}()
   933  			}
   934  
   935  			// FIXME: since we only need to recheck files protected rules, we could improve this
   936  			matchedBranches, err := git_model.FindAllMatchedBranches(ctx, ctx.Repo.Repository.ID, protectBranch.RuleName)
   937  			if err != nil {
   938  				ctx.Error(http.StatusInternalServerError, "FindAllMatchedBranches", err)
   939  				return
   940  			}
   941  
   942  			for _, branchName := range matchedBranches {
   943  				if err = pull_service.CheckPRsForBaseBranch(ctx, ctx.Repo.Repository, branchName); err != nil {
   944  					ctx.Error(http.StatusInternalServerError, "CheckPrsForBaseBranch", err)
   945  					return
   946  				}
   947  			}
   948  		}
   949  	}
   950  
   951  	// Reload from db to ensure get all whitelists
   952  	bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
   953  	if err != nil {
   954  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchBy", err)
   955  		return
   956  	}
   957  	if bp == nil || bp.RepoID != ctx.Repo.Repository.ID {
   958  		ctx.Error(http.StatusInternalServerError, "New branch protection not found", err)
   959  		return
   960  	}
   961  
   962  	ctx.JSON(http.StatusOK, convert.ToBranchProtection(ctx, bp))
   963  }
   964  
   965  // DeleteBranchProtection deletes a branch protection for a repo
   966  func DeleteBranchProtection(ctx *context.APIContext) {
   967  	// swagger:operation DELETE /repos/{owner}/{repo}/branch_protections/{name} repository repoDeleteBranchProtection
   968  	// ---
   969  	// summary: Delete a specific branch protection for the repository
   970  	// produces:
   971  	// - application/json
   972  	// parameters:
   973  	// - name: owner
   974  	//   in: path
   975  	//   description: owner of the repo
   976  	//   type: string
   977  	//   required: true
   978  	// - name: repo
   979  	//   in: path
   980  	//   description: name of the repo
   981  	//   type: string
   982  	//   required: true
   983  	// - name: name
   984  	//   in: path
   985  	//   description: name of protected branch
   986  	//   type: string
   987  	//   required: true
   988  	// responses:
   989  	//   "204":
   990  	//     "$ref": "#/responses/empty"
   991  	//   "404":
   992  	//     "$ref": "#/responses/notFound"
   993  
   994  	repo := ctx.Repo.Repository
   995  	bpName := ctx.Params(":name")
   996  	bp, err := git_model.GetProtectedBranchRuleByName(ctx, repo.ID, bpName)
   997  	if err != nil {
   998  		ctx.Error(http.StatusInternalServerError, "GetProtectedBranchByID", err)
   999  		return
  1000  	}
  1001  	if bp == nil || bp.RepoID != repo.ID {
  1002  		ctx.NotFound()
  1003  		return
  1004  	}
  1005  
  1006  	if err := git_model.DeleteProtectedBranch(ctx, ctx.Repo.Repository.ID, bp.ID); err != nil {
  1007  		ctx.Error(http.StatusInternalServerError, "DeleteProtectedBranch", err)
  1008  		return
  1009  	}
  1010  
  1011  	ctx.Status(http.StatusNoContent)
  1012  }