code.gitea.io/gitea@v1.21.7/routers/web/repo/pull.go (about)

     1  // Copyright 2018 The Gitea Authors.
     2  // Copyright 2014 The Gogs Authors.
     3  // All rights reserved.
     4  // SPDX-License-Identifier: MIT
     5  
     6  package repo
     7  
     8  import (
     9  	"errors"
    10  	"fmt"
    11  	"html"
    12  	"net/http"
    13  	"net/url"
    14  	"strconv"
    15  	"strings"
    16  	"time"
    17  
    18  	"code.gitea.io/gitea/models"
    19  	activities_model "code.gitea.io/gitea/models/activities"
    20  	"code.gitea.io/gitea/models/db"
    21  	git_model "code.gitea.io/gitea/models/git"
    22  	issues_model "code.gitea.io/gitea/models/issues"
    23  	"code.gitea.io/gitea/models/organization"
    24  	access_model "code.gitea.io/gitea/models/perm/access"
    25  	pull_model "code.gitea.io/gitea/models/pull"
    26  	repo_model "code.gitea.io/gitea/models/repo"
    27  	"code.gitea.io/gitea/models/unit"
    28  	user_model "code.gitea.io/gitea/models/user"
    29  	"code.gitea.io/gitea/modules/base"
    30  	"code.gitea.io/gitea/modules/context"
    31  	"code.gitea.io/gitea/modules/git"
    32  	issue_template "code.gitea.io/gitea/modules/issue/template"
    33  	"code.gitea.io/gitea/modules/log"
    34  	"code.gitea.io/gitea/modules/setting"
    35  	"code.gitea.io/gitea/modules/structs"
    36  	"code.gitea.io/gitea/modules/upload"
    37  	"code.gitea.io/gitea/modules/util"
    38  	"code.gitea.io/gitea/modules/web"
    39  	"code.gitea.io/gitea/routers/utils"
    40  	asymkey_service "code.gitea.io/gitea/services/asymkey"
    41  	"code.gitea.io/gitea/services/automerge"
    42  	"code.gitea.io/gitea/services/forms"
    43  	"code.gitea.io/gitea/services/gitdiff"
    44  	notify_service "code.gitea.io/gitea/services/notify"
    45  	pull_service "code.gitea.io/gitea/services/pull"
    46  	repo_service "code.gitea.io/gitea/services/repository"
    47  
    48  	"github.com/gobwas/glob"
    49  )
    50  
    51  const (
    52  	tplFork        base.TplName = "repo/pulls/fork"
    53  	tplCompareDiff base.TplName = "repo/diff/compare"
    54  	tplPullCommits base.TplName = "repo/pulls/commits"
    55  	tplPullFiles   base.TplName = "repo/pulls/files"
    56  
    57  	pullRequestTemplateKey = "PullRequestTemplate"
    58  )
    59  
    60  var pullRequestTemplateCandidates = []string{
    61  	"PULL_REQUEST_TEMPLATE.md",
    62  	"PULL_REQUEST_TEMPLATE.yaml",
    63  	"PULL_REQUEST_TEMPLATE.yml",
    64  	"pull_request_template.md",
    65  	"pull_request_template.yaml",
    66  	"pull_request_template.yml",
    67  	".gitea/PULL_REQUEST_TEMPLATE.md",
    68  	".gitea/PULL_REQUEST_TEMPLATE.yaml",
    69  	".gitea/PULL_REQUEST_TEMPLATE.yml",
    70  	".gitea/pull_request_template.md",
    71  	".gitea/pull_request_template.yaml",
    72  	".gitea/pull_request_template.yml",
    73  	".github/PULL_REQUEST_TEMPLATE.md",
    74  	".github/PULL_REQUEST_TEMPLATE.yaml",
    75  	".github/PULL_REQUEST_TEMPLATE.yml",
    76  	".github/pull_request_template.md",
    77  	".github/pull_request_template.yaml",
    78  	".github/pull_request_template.yml",
    79  }
    80  
    81  func getRepository(ctx *context.Context, repoID int64) *repo_model.Repository {
    82  	repo, err := repo_model.GetRepositoryByID(ctx, repoID)
    83  	if err != nil {
    84  		if repo_model.IsErrRepoNotExist(err) {
    85  			ctx.NotFound("GetRepositoryByID", nil)
    86  		} else {
    87  			ctx.ServerError("GetRepositoryByID", err)
    88  		}
    89  		return nil
    90  	}
    91  
    92  	perm, err := access_model.GetUserRepoPermission(ctx, repo, ctx.Doer)
    93  	if err != nil {
    94  		ctx.ServerError("GetUserRepoPermission", err)
    95  		return nil
    96  	}
    97  
    98  	if !perm.CanRead(unit.TypeCode) {
    99  		log.Trace("Permission Denied: User %-v cannot read %-v of repo %-v\n"+
   100  			"User in repo has Permissions: %-+v",
   101  			ctx.Doer,
   102  			unit.TypeCode,
   103  			ctx.Repo,
   104  			perm)
   105  		ctx.NotFound("getRepository", nil)
   106  		return nil
   107  	}
   108  	return repo
   109  }
   110  
   111  func getForkRepository(ctx *context.Context) *repo_model.Repository {
   112  	forkRepo := getRepository(ctx, ctx.ParamsInt64(":repoid"))
   113  	if ctx.Written() {
   114  		return nil
   115  	}
   116  
   117  	if forkRepo.IsEmpty {
   118  		log.Trace("Empty repository %-v", forkRepo)
   119  		ctx.NotFound("getForkRepository", nil)
   120  		return nil
   121  	}
   122  
   123  	if err := forkRepo.LoadOwner(ctx); err != nil {
   124  		ctx.ServerError("LoadOwner", err)
   125  		return nil
   126  	}
   127  
   128  	ctx.Data["repo_name"] = forkRepo.Name
   129  	ctx.Data["description"] = forkRepo.Description
   130  	ctx.Data["IsPrivate"] = forkRepo.IsPrivate || forkRepo.Owner.Visibility == structs.VisibleTypePrivate
   131  	canForkToUser := forkRepo.OwnerID != ctx.Doer.ID && !repo_model.HasForkedRepo(ctx, ctx.Doer.ID, forkRepo.ID)
   132  
   133  	ctx.Data["ForkRepo"] = forkRepo
   134  
   135  	ownedOrgs, err := organization.GetOrgsCanCreateRepoByUserID(ctx, ctx.Doer.ID)
   136  	if err != nil {
   137  		ctx.ServerError("GetOrgsCanCreateRepoByUserID", err)
   138  		return nil
   139  	}
   140  	var orgs []*organization.Organization
   141  	for _, org := range ownedOrgs {
   142  		if forkRepo.OwnerID != org.ID && !repo_model.HasForkedRepo(ctx, org.ID, forkRepo.ID) {
   143  			orgs = append(orgs, org)
   144  		}
   145  	}
   146  
   147  	traverseParentRepo := forkRepo
   148  	for {
   149  		if ctx.Doer.ID == traverseParentRepo.OwnerID {
   150  			canForkToUser = false
   151  		} else {
   152  			for i, org := range orgs {
   153  				if org.ID == traverseParentRepo.OwnerID {
   154  					orgs = append(orgs[:i], orgs[i+1:]...)
   155  					break
   156  				}
   157  			}
   158  		}
   159  
   160  		if !traverseParentRepo.IsFork {
   161  			break
   162  		}
   163  		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
   164  		if err != nil {
   165  			ctx.ServerError("GetRepositoryByID", err)
   166  			return nil
   167  		}
   168  	}
   169  
   170  	ctx.Data["CanForkToUser"] = canForkToUser
   171  	ctx.Data["Orgs"] = orgs
   172  
   173  	if canForkToUser {
   174  		ctx.Data["ContextUser"] = ctx.Doer
   175  	} else if len(orgs) > 0 {
   176  		ctx.Data["ContextUser"] = orgs[0]
   177  	} else {
   178  		ctx.Data["CanForkRepo"] = false
   179  		ctx.Flash.Error(ctx.Tr("repo.fork_no_valid_owners"), true)
   180  		return nil
   181  	}
   182  
   183  	return forkRepo
   184  }
   185  
   186  // Fork render repository fork page
   187  func Fork(ctx *context.Context) {
   188  	ctx.Data["Title"] = ctx.Tr("new_fork")
   189  
   190  	if ctx.Doer.CanForkRepo() {
   191  		ctx.Data["CanForkRepo"] = true
   192  	} else {
   193  		maxCreationLimit := ctx.Doer.MaxCreationLimit()
   194  		msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
   195  		ctx.Flash.Error(msg, true)
   196  	}
   197  
   198  	getForkRepository(ctx)
   199  	if ctx.Written() {
   200  		return
   201  	}
   202  
   203  	ctx.HTML(http.StatusOK, tplFork)
   204  }
   205  
   206  // ForkPost response for forking a repository
   207  func ForkPost(ctx *context.Context) {
   208  	form := web.GetForm(ctx).(*forms.CreateRepoForm)
   209  	ctx.Data["Title"] = ctx.Tr("new_fork")
   210  	ctx.Data["CanForkRepo"] = true
   211  
   212  	ctxUser := checkContextUser(ctx, form.UID)
   213  	if ctx.Written() {
   214  		return
   215  	}
   216  
   217  	forkRepo := getForkRepository(ctx)
   218  	if ctx.Written() {
   219  		return
   220  	}
   221  
   222  	ctx.Data["ContextUser"] = ctxUser
   223  
   224  	if ctx.HasError() {
   225  		ctx.HTML(http.StatusOK, tplFork)
   226  		return
   227  	}
   228  
   229  	var err error
   230  	traverseParentRepo := forkRepo
   231  	for {
   232  		if ctxUser.ID == traverseParentRepo.OwnerID {
   233  			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
   234  			return
   235  		}
   236  		repo := repo_model.GetForkedRepo(ctx, ctxUser.ID, traverseParentRepo.ID)
   237  		if repo != nil {
   238  			ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
   239  			return
   240  		}
   241  		if !traverseParentRepo.IsFork {
   242  			break
   243  		}
   244  		traverseParentRepo, err = repo_model.GetRepositoryByID(ctx, traverseParentRepo.ForkID)
   245  		if err != nil {
   246  			ctx.ServerError("GetRepositoryByID", err)
   247  			return
   248  		}
   249  	}
   250  
   251  	// Check if user is allowed to create repo's on the organization.
   252  	if ctxUser.IsOrganization() {
   253  		isAllowedToFork, err := organization.OrgFromUser(ctxUser).CanCreateOrgRepo(ctx.Doer.ID)
   254  		if err != nil {
   255  			ctx.ServerError("CanCreateOrgRepo", err)
   256  			return
   257  		} else if !isAllowedToFork {
   258  			ctx.Error(http.StatusForbidden)
   259  			return
   260  		}
   261  	}
   262  
   263  	repo, err := repo_service.ForkRepository(ctx, ctx.Doer, ctxUser, repo_service.ForkRepoOptions{
   264  		BaseRepo:    forkRepo,
   265  		Name:        form.RepoName,
   266  		Description: form.Description,
   267  	})
   268  	if err != nil {
   269  		ctx.Data["Err_RepoName"] = true
   270  		switch {
   271  		case repo_model.IsErrReachLimitOfRepo(err):
   272  			maxCreationLimit := ctxUser.MaxCreationLimit()
   273  			msg := ctx.TrN(maxCreationLimit, "repo.form.reach_limit_of_creation_1", "repo.form.reach_limit_of_creation_n", maxCreationLimit)
   274  			ctx.RenderWithErr(msg, tplFork, &form)
   275  		case repo_model.IsErrRepoAlreadyExist(err):
   276  			ctx.RenderWithErr(ctx.Tr("repo.settings.new_owner_has_same_repo"), tplFork, &form)
   277  		case repo_model.IsErrRepoFilesAlreadyExist(err):
   278  			switch {
   279  			case ctx.IsUserSiteAdmin() || (setting.Repository.AllowAdoptionOfUnadoptedRepositories && setting.Repository.AllowDeleteOfUnadoptedRepositories):
   280  				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt_or_delete"), tplFork, form)
   281  			case setting.Repository.AllowAdoptionOfUnadoptedRepositories:
   282  				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.adopt"), tplFork, form)
   283  			case setting.Repository.AllowDeleteOfUnadoptedRepositories:
   284  				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist.delete"), tplFork, form)
   285  			default:
   286  				ctx.RenderWithErr(ctx.Tr("form.repository_files_already_exist"), tplFork, form)
   287  			}
   288  		case db.IsErrNameReserved(err):
   289  			ctx.RenderWithErr(ctx.Tr("repo.form.name_reserved", err.(db.ErrNameReserved).Name), tplFork, &form)
   290  		case db.IsErrNamePatternNotAllowed(err):
   291  			ctx.RenderWithErr(ctx.Tr("repo.form.name_pattern_not_allowed", err.(db.ErrNamePatternNotAllowed).Pattern), tplFork, &form)
   292  		default:
   293  			ctx.ServerError("ForkPost", err)
   294  		}
   295  		return
   296  	}
   297  
   298  	log.Trace("Repository forked[%d]: %s/%s", forkRepo.ID, ctxUser.Name, repo.Name)
   299  	ctx.Redirect(ctxUser.HomeLink() + "/" + url.PathEscape(repo.Name))
   300  }
   301  
   302  func getPullInfo(ctx *context.Context) (issue *issues_model.Issue, ok bool) {
   303  	issue, err := issues_model.GetIssueByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
   304  	if err != nil {
   305  		if issues_model.IsErrIssueNotExist(err) {
   306  			ctx.NotFound("GetIssueByIndex", err)
   307  		} else {
   308  			ctx.ServerError("GetIssueByIndex", err)
   309  		}
   310  		return nil, false
   311  	}
   312  	if err = issue.LoadPoster(ctx); err != nil {
   313  		ctx.ServerError("LoadPoster", err)
   314  		return nil, false
   315  	}
   316  	if err := issue.LoadRepo(ctx); err != nil {
   317  		ctx.ServerError("LoadRepo", err)
   318  		return nil, false
   319  	}
   320  	ctx.Data["Title"] = fmt.Sprintf("#%d - %s", issue.Index, issue.Title)
   321  	ctx.Data["Issue"] = issue
   322  
   323  	if !issue.IsPull {
   324  		ctx.NotFound("ViewPullCommits", nil)
   325  		return nil, false
   326  	}
   327  
   328  	if err = issue.LoadPullRequest(ctx); err != nil {
   329  		ctx.ServerError("LoadPullRequest", err)
   330  		return nil, false
   331  	}
   332  
   333  	if err = issue.PullRequest.LoadHeadRepo(ctx); err != nil {
   334  		ctx.ServerError("LoadHeadRepo", err)
   335  		return nil, false
   336  	}
   337  
   338  	if ctx.IsSigned {
   339  		// Update issue-user.
   340  		if err = activities_model.SetIssueReadBy(ctx, issue.ID, ctx.Doer.ID); err != nil {
   341  			ctx.ServerError("ReadBy", err)
   342  			return nil, false
   343  		}
   344  	}
   345  
   346  	return issue, true
   347  }
   348  
   349  func setMergeTarget(ctx *context.Context, pull *issues_model.PullRequest) {
   350  	if ctx.Repo.Owner.Name == pull.MustHeadUserName(ctx) {
   351  		ctx.Data["HeadTarget"] = pull.HeadBranch
   352  	} else if pull.HeadRepo == nil {
   353  		ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + ":" + pull.HeadBranch
   354  	} else {
   355  		ctx.Data["HeadTarget"] = pull.MustHeadUserName(ctx) + "/" + pull.HeadRepo.Name + ":" + pull.HeadBranch
   356  	}
   357  	ctx.Data["BaseTarget"] = pull.BaseBranch
   358  	ctx.Data["HeadBranchLink"] = pull.GetHeadBranchLink()
   359  	ctx.Data["BaseBranchLink"] = pull.GetBaseBranchLink()
   360  }
   361  
   362  // GetPullDiffStats get Pull Requests diff stats
   363  func GetPullDiffStats(ctx *context.Context) {
   364  	issue, ok := getPullInfo(ctx)
   365  	if !ok {
   366  		return
   367  	}
   368  	pull := issue.PullRequest
   369  
   370  	mergeBaseCommitID := GetMergedBaseCommitID(ctx, issue)
   371  
   372  	if mergeBaseCommitID == "" {
   373  		ctx.NotFound("PullFiles", nil)
   374  		return
   375  	}
   376  
   377  	headCommitID, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
   378  	if err != nil {
   379  		ctx.ServerError("GetRefCommitID", err)
   380  		return
   381  	}
   382  
   383  	diffOptions := &gitdiff.DiffOptions{
   384  		BeforeCommitID:     mergeBaseCommitID,
   385  		AfterCommitID:      headCommitID,
   386  		MaxLines:           setting.Git.MaxGitDiffLines,
   387  		MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
   388  		MaxFiles:           setting.Git.MaxGitDiffFiles,
   389  		WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
   390  	}
   391  
   392  	diff, err := gitdiff.GetPullDiffStats(ctx.Repo.GitRepo, diffOptions)
   393  	if err != nil {
   394  		ctx.ServerError("GetPullDiffStats", err)
   395  		return
   396  	}
   397  
   398  	ctx.Data["Diff"] = diff
   399  }
   400  
   401  func GetMergedBaseCommitID(ctx *context.Context, issue *issues_model.Issue) string {
   402  	pull := issue.PullRequest
   403  
   404  	var baseCommit string
   405  	// Some migrated PR won't have any Base SHA and lose history, try to get one
   406  	if pull.MergeBase == "" {
   407  		var commitSHA, parentCommit string
   408  		// If there is a head or a patch file, and it is readable, grab info
   409  		commitSHA, err := ctx.Repo.GitRepo.GetRefCommitID(pull.GetGitRefName())
   410  		if err != nil {
   411  			// Head File does not exist, try the patch
   412  			commitSHA, err = ctx.Repo.GitRepo.ReadPatchCommit(pull.Index)
   413  			if err == nil {
   414  				// Recreate pull head in files for next time
   415  				if err := ctx.Repo.GitRepo.SetReference(pull.GetGitRefName(), commitSHA); err != nil {
   416  					log.Error("Could not write head file", err)
   417  				}
   418  			} else {
   419  				// There is no history available
   420  				log.Trace("No history file available for PR %d", pull.Index)
   421  			}
   422  		}
   423  		if commitSHA != "" {
   424  			// Get immediate parent of the first commit in the patch, grab history back
   425  			parentCommit, _, err = git.NewCommand(ctx, "rev-list", "-1", "--skip=1").AddDynamicArguments(commitSHA).RunStdString(&git.RunOpts{Dir: ctx.Repo.GitRepo.Path})
   426  			if err == nil {
   427  				parentCommit = strings.TrimSpace(parentCommit)
   428  			}
   429  			// Special case on Git < 2.25 that doesn't fail on immediate empty history
   430  			if err != nil || parentCommit == "" {
   431  				log.Info("No known parent commit for PR %d, error: %v", pull.Index, err)
   432  				// bring at least partial history if it can work
   433  				parentCommit = commitSHA
   434  			}
   435  		}
   436  		baseCommit = parentCommit
   437  	} else {
   438  		// Keep an empty history or original commit
   439  		baseCommit = pull.MergeBase
   440  	}
   441  
   442  	return baseCommit
   443  }
   444  
   445  // PrepareMergedViewPullInfo show meta information for a merged pull request view page
   446  func PrepareMergedViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
   447  	pull := issue.PullRequest
   448  
   449  	setMergeTarget(ctx, pull)
   450  	ctx.Data["HasMerged"] = true
   451  
   452  	baseCommit := GetMergedBaseCommitID(ctx, issue)
   453  
   454  	compareInfo, err := ctx.Repo.GitRepo.GetCompareInfo(ctx.Repo.Repository.RepoPath(),
   455  		baseCommit, pull.GetGitRefName(), false, false)
   456  	if err != nil {
   457  		if strings.Contains(err.Error(), "fatal: Not a valid object name") || strings.Contains(err.Error(), "unknown revision or path not in the working tree") {
   458  			ctx.Data["IsPullRequestBroken"] = true
   459  			ctx.Data["BaseTarget"] = pull.BaseBranch
   460  			ctx.Data["NumCommits"] = 0
   461  			ctx.Data["NumFiles"] = 0
   462  			return nil
   463  		}
   464  
   465  		ctx.ServerError("GetCompareInfo", err)
   466  		return nil
   467  	}
   468  	ctx.Data["NumCommits"] = len(compareInfo.Commits)
   469  	ctx.Data["NumFiles"] = compareInfo.NumFiles
   470  
   471  	if len(compareInfo.Commits) != 0 {
   472  		sha := compareInfo.Commits[0].ID.String()
   473  		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, ctx.Repo.Repository.ID, sha, db.ListOptions{ListAll: true})
   474  		if err != nil {
   475  			ctx.ServerError("GetLatestCommitStatus", err)
   476  			return nil
   477  		}
   478  		if len(commitStatuses) != 0 {
   479  			ctx.Data["LatestCommitStatuses"] = commitStatuses
   480  			ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
   481  		}
   482  	}
   483  
   484  	return compareInfo
   485  }
   486  
   487  // PrepareViewPullInfo show meta information for a pull request preview page
   488  func PrepareViewPullInfo(ctx *context.Context, issue *issues_model.Issue) *git.CompareInfo {
   489  	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
   490  
   491  	repo := ctx.Repo.Repository
   492  	pull := issue.PullRequest
   493  
   494  	if err := pull.LoadHeadRepo(ctx); err != nil {
   495  		ctx.ServerError("LoadHeadRepo", err)
   496  		return nil
   497  	}
   498  
   499  	if err := pull.LoadBaseRepo(ctx); err != nil {
   500  		ctx.ServerError("LoadBaseRepo", err)
   501  		return nil
   502  	}
   503  
   504  	setMergeTarget(ctx, pull)
   505  
   506  	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, repo.ID, pull.BaseBranch)
   507  	if err != nil {
   508  		ctx.ServerError("LoadProtectedBranch", err)
   509  		return nil
   510  	}
   511  	ctx.Data["EnableStatusCheck"] = pb != nil && pb.EnableStatusCheck
   512  
   513  	var baseGitRepo *git.Repository
   514  	if pull.BaseRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
   515  		baseGitRepo = ctx.Repo.GitRepo
   516  	} else {
   517  		baseGitRepo, err := git.OpenRepository(ctx, pull.BaseRepo.RepoPath())
   518  		if err != nil {
   519  			ctx.ServerError("OpenRepository", err)
   520  			return nil
   521  		}
   522  		defer baseGitRepo.Close()
   523  	}
   524  
   525  	if !baseGitRepo.IsBranchExist(pull.BaseBranch) {
   526  		ctx.Data["IsPullRequestBroken"] = true
   527  		ctx.Data["BaseTarget"] = pull.BaseBranch
   528  		ctx.Data["HeadTarget"] = pull.HeadBranch
   529  
   530  		sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
   531  		if err != nil {
   532  			ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
   533  			return nil
   534  		}
   535  		commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
   536  		if err != nil {
   537  			ctx.ServerError("GetLatestCommitStatus", err)
   538  			return nil
   539  		}
   540  		if len(commitStatuses) > 0 {
   541  			ctx.Data["LatestCommitStatuses"] = commitStatuses
   542  			ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
   543  		}
   544  
   545  		compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
   546  			pull.MergeBase, pull.GetGitRefName(), false, false)
   547  		if err != nil {
   548  			if strings.Contains(err.Error(), "fatal: Not a valid object name") {
   549  				ctx.Data["IsPullRequestBroken"] = true
   550  				ctx.Data["BaseTarget"] = pull.BaseBranch
   551  				ctx.Data["NumCommits"] = 0
   552  				ctx.Data["NumFiles"] = 0
   553  				return nil
   554  			}
   555  
   556  			ctx.ServerError("GetCompareInfo", err)
   557  			return nil
   558  		}
   559  
   560  		ctx.Data["NumCommits"] = len(compareInfo.Commits)
   561  		ctx.Data["NumFiles"] = compareInfo.NumFiles
   562  		return compareInfo
   563  	}
   564  
   565  	var headBranchExist bool
   566  	var headBranchSha string
   567  	// HeadRepo may be missing
   568  	if pull.HeadRepo != nil {
   569  		headGitRepo, err := git.OpenRepository(ctx, pull.HeadRepo.RepoPath())
   570  		if err != nil {
   571  			ctx.ServerError("OpenRepository", err)
   572  			return nil
   573  		}
   574  		defer headGitRepo.Close()
   575  
   576  		if pull.Flow == issues_model.PullRequestFlowGithub {
   577  			headBranchExist = headGitRepo.IsBranchExist(pull.HeadBranch)
   578  		} else {
   579  			headBranchExist = git.IsReferenceExist(ctx, baseGitRepo.Path, pull.GetGitRefName())
   580  		}
   581  
   582  		if headBranchExist {
   583  			if pull.Flow != issues_model.PullRequestFlowGithub {
   584  				headBranchSha, err = baseGitRepo.GetRefCommitID(pull.GetGitRefName())
   585  			} else {
   586  				headBranchSha, err = headGitRepo.GetBranchCommitID(pull.HeadBranch)
   587  			}
   588  			if err != nil {
   589  				ctx.ServerError("GetBranchCommitID", err)
   590  				return nil
   591  			}
   592  		}
   593  	}
   594  
   595  	if headBranchExist {
   596  		var err error
   597  		ctx.Data["UpdateAllowed"], ctx.Data["UpdateByRebaseAllowed"], err = pull_service.IsUserAllowedToUpdate(ctx, pull, ctx.Doer)
   598  		if err != nil {
   599  			ctx.ServerError("IsUserAllowedToUpdate", err)
   600  			return nil
   601  		}
   602  		ctx.Data["GetCommitMessages"] = pull_service.GetSquashMergeCommitMessages(ctx, pull)
   603  	} else {
   604  		ctx.Data["GetCommitMessages"] = ""
   605  	}
   606  
   607  	sha, err := baseGitRepo.GetRefCommitID(pull.GetGitRefName())
   608  	if err != nil {
   609  		if git.IsErrNotExist(err) {
   610  			ctx.Data["IsPullRequestBroken"] = true
   611  			if pull.IsSameRepo() {
   612  				ctx.Data["HeadTarget"] = pull.HeadBranch
   613  			} else if pull.HeadRepo == nil {
   614  				ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch)
   615  			} else {
   616  				ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
   617  			}
   618  			ctx.Data["BaseTarget"] = pull.BaseBranch
   619  			ctx.Data["NumCommits"] = 0
   620  			ctx.Data["NumFiles"] = 0
   621  			return nil
   622  		}
   623  		ctx.ServerError(fmt.Sprintf("GetRefCommitID(%s)", pull.GetGitRefName()), err)
   624  		return nil
   625  	}
   626  
   627  	commitStatuses, _, err := git_model.GetLatestCommitStatus(ctx, repo.ID, sha, db.ListOptions{ListAll: true})
   628  	if err != nil {
   629  		ctx.ServerError("GetLatestCommitStatus", err)
   630  		return nil
   631  	}
   632  	if len(commitStatuses) > 0 {
   633  		ctx.Data["LatestCommitStatuses"] = commitStatuses
   634  		ctx.Data["LatestCommitStatus"] = git_model.CalcCommitStatus(commitStatuses)
   635  	}
   636  
   637  	if pb != nil && pb.EnableStatusCheck {
   638  
   639  		var missingRequiredChecks []string
   640  		for _, requiredContext := range pb.StatusCheckContexts {
   641  			contextFound := false
   642  			matchesRequiredContext := createRequiredContextMatcher(requiredContext)
   643  			for _, presentStatus := range commitStatuses {
   644  				if matchesRequiredContext(presentStatus.Context) {
   645  					contextFound = true
   646  					break
   647  				}
   648  			}
   649  
   650  			if !contextFound {
   651  				missingRequiredChecks = append(missingRequiredChecks, requiredContext)
   652  			}
   653  		}
   654  		ctx.Data["MissingRequiredChecks"] = missingRequiredChecks
   655  
   656  		ctx.Data["is_context_required"] = func(context string) bool {
   657  			for _, c := range pb.StatusCheckContexts {
   658  				if c == context {
   659  					return true
   660  				}
   661  				if gp, err := glob.Compile(c); err != nil {
   662  					// All newly created status_check_contexts are checked to ensure they are valid glob expressions before being stored in the database.
   663  					// But some old status_check_context created before glob was introduced may be invalid glob expressions.
   664  					// So log the error here for debugging.
   665  					log.Error("compile glob %q: %v", c, err)
   666  				} else if gp.Match(context) {
   667  					return true
   668  				}
   669  			}
   670  			return false
   671  		}
   672  		ctx.Data["RequiredStatusCheckState"] = pull_service.MergeRequiredContextsCommitStatus(commitStatuses, pb.StatusCheckContexts)
   673  	}
   674  
   675  	ctx.Data["HeadBranchMovedOn"] = headBranchSha != sha
   676  	ctx.Data["HeadBranchCommitID"] = headBranchSha
   677  	ctx.Data["PullHeadCommitID"] = sha
   678  
   679  	if pull.HeadRepo == nil || !headBranchExist || (!pull.Issue.IsClosed && (headBranchSha != sha)) {
   680  		ctx.Data["IsPullRequestBroken"] = true
   681  		if pull.IsSameRepo() {
   682  			ctx.Data["HeadTarget"] = pull.HeadBranch
   683  		} else if pull.HeadRepo == nil {
   684  			ctx.Data["HeadTarget"] = ctx.Locale.Tr("repo.pull.deleted_branch", pull.HeadBranch)
   685  		} else {
   686  			ctx.Data["HeadTarget"] = pull.HeadRepo.OwnerName + ":" + pull.HeadBranch
   687  		}
   688  	}
   689  
   690  	compareInfo, err := baseGitRepo.GetCompareInfo(pull.BaseRepo.RepoPath(),
   691  		git.BranchPrefix+pull.BaseBranch, pull.GetGitRefName(), false, false)
   692  	if err != nil {
   693  		if strings.Contains(err.Error(), "fatal: Not a valid object name") {
   694  			ctx.Data["IsPullRequestBroken"] = true
   695  			ctx.Data["BaseTarget"] = pull.BaseBranch
   696  			ctx.Data["NumCommits"] = 0
   697  			ctx.Data["NumFiles"] = 0
   698  			return nil
   699  		}
   700  
   701  		ctx.ServerError("GetCompareInfo", err)
   702  		return nil
   703  	}
   704  
   705  	if compareInfo.HeadCommitID == compareInfo.MergeBase {
   706  		ctx.Data["IsNothingToCompare"] = true
   707  	}
   708  
   709  	if pull.IsWorkInProgress() {
   710  		ctx.Data["IsPullWorkInProgress"] = true
   711  		ctx.Data["WorkInProgressPrefix"] = pull.GetWorkInProgressPrefix(ctx)
   712  	}
   713  
   714  	if pull.IsFilesConflicted() {
   715  		ctx.Data["IsPullFilesConflicted"] = true
   716  		ctx.Data["ConflictedFiles"] = pull.ConflictedFiles
   717  	}
   718  
   719  	ctx.Data["NumCommits"] = len(compareInfo.Commits)
   720  	ctx.Data["NumFiles"] = compareInfo.NumFiles
   721  	return compareInfo
   722  }
   723  
   724  func createRequiredContextMatcher(requiredContext string) func(string) bool {
   725  	if gp, err := glob.Compile(requiredContext); err == nil {
   726  		return func(contextToCheck string) bool {
   727  			return gp.Match(contextToCheck)
   728  		}
   729  	}
   730  
   731  	return func(contextToCheck string) bool {
   732  		return requiredContext == contextToCheck
   733  	}
   734  }
   735  
   736  type pullCommitList struct {
   737  	Commits             []pull_service.CommitInfo `json:"commits"`
   738  	LastReviewCommitSha string                    `json:"last_review_commit_sha"`
   739  	Locale              map[string]string         `json:"locale"`
   740  }
   741  
   742  // GetPullCommits get all commits for given pull request
   743  func GetPullCommits(ctx *context.Context) {
   744  	issue, ok := getPullInfo(ctx)
   745  	if !ok {
   746  		return
   747  	}
   748  	resp := &pullCommitList{}
   749  
   750  	commits, lastReviewCommitSha, err := pull_service.GetPullCommits(ctx, issue)
   751  	if err != nil {
   752  		ctx.JSON(http.StatusInternalServerError, err)
   753  		return
   754  	}
   755  
   756  	// Get the needed locale
   757  	resp.Locale = map[string]string{
   758  		"lang":                                ctx.Locale.Language(),
   759  		"show_all_commits":                    ctx.Tr("repo.pulls.show_all_commits"),
   760  		"stats_num_commits":                   ctx.TrN(len(commits), "repo.activity.git_stats_commit_1", "repo.activity.git_stats_commit_n", len(commits)),
   761  		"show_changes_since_your_last_review": ctx.Tr("repo.pulls.show_changes_since_your_last_review"),
   762  		"select_commit_hold_shift_for_range":  ctx.Tr("repo.pulls.select_commit_hold_shift_for_range"),
   763  	}
   764  
   765  	resp.Commits = commits
   766  	resp.LastReviewCommitSha = lastReviewCommitSha
   767  
   768  	ctx.JSON(http.StatusOK, resp)
   769  }
   770  
   771  // ViewPullCommits show commits for a pull request
   772  func ViewPullCommits(ctx *context.Context) {
   773  	ctx.Data["PageIsPullList"] = true
   774  	ctx.Data["PageIsPullCommits"] = true
   775  
   776  	issue, ok := getPullInfo(ctx)
   777  	if !ok {
   778  		return
   779  	}
   780  	pull := issue.PullRequest
   781  
   782  	var prInfo *git.CompareInfo
   783  	if pull.HasMerged {
   784  		prInfo = PrepareMergedViewPullInfo(ctx, issue)
   785  	} else {
   786  		prInfo = PrepareViewPullInfo(ctx, issue)
   787  	}
   788  
   789  	if ctx.Written() {
   790  		return
   791  	} else if prInfo == nil {
   792  		ctx.NotFound("ViewPullCommits", nil)
   793  		return
   794  	}
   795  
   796  	ctx.Data["Username"] = ctx.Repo.Owner.Name
   797  	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
   798  
   799  	commits := git_model.ConvertFromGitCommit(ctx, prInfo.Commits, ctx.Repo.Repository)
   800  	ctx.Data["Commits"] = commits
   801  	ctx.Data["CommitCount"] = len(commits)
   802  
   803  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
   804  	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
   805  
   806  	// For PR commits page
   807  	PrepareBranchList(ctx)
   808  	if ctx.Written() {
   809  		return
   810  	}
   811  	getBranchData(ctx, issue)
   812  	ctx.HTML(http.StatusOK, tplPullCommits)
   813  }
   814  
   815  // ViewPullFiles render pull request changed files list page
   816  func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommit string, willShowSpecifiedCommitRange, willShowSpecifiedCommit bool) {
   817  	ctx.Data["PageIsPullList"] = true
   818  	ctx.Data["PageIsPullFiles"] = true
   819  
   820  	issue, ok := getPullInfo(ctx)
   821  	if !ok {
   822  		return
   823  	}
   824  	pull := issue.PullRequest
   825  
   826  	var (
   827  		startCommitID string
   828  		endCommitID   string
   829  		gitRepo       = ctx.Repo.GitRepo
   830  	)
   831  
   832  	var prInfo *git.CompareInfo
   833  	if pull.HasMerged {
   834  		prInfo = PrepareMergedViewPullInfo(ctx, issue)
   835  	} else {
   836  		prInfo = PrepareViewPullInfo(ctx, issue)
   837  	}
   838  
   839  	// Validate the given commit sha to show (if any passed)
   840  	if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
   841  
   842  		foundStartCommit := len(specifiedStartCommit) == 0
   843  		foundEndCommit := len(specifiedEndCommit) == 0
   844  
   845  		if !(foundStartCommit && foundEndCommit) {
   846  			for _, commit := range prInfo.Commits {
   847  				if commit.ID.String() == specifiedStartCommit {
   848  					foundStartCommit = true
   849  				}
   850  				if commit.ID.String() == specifiedEndCommit {
   851  					foundEndCommit = true
   852  				}
   853  
   854  				if foundStartCommit && foundEndCommit {
   855  					break
   856  				}
   857  			}
   858  		}
   859  
   860  		if !(foundStartCommit && foundEndCommit) {
   861  			ctx.NotFound("Given SHA1 not found for this PR", nil)
   862  			return
   863  		}
   864  	}
   865  
   866  	if ctx.Written() {
   867  		return
   868  	} else if prInfo == nil {
   869  		ctx.NotFound("ViewPullFiles", nil)
   870  		return
   871  	}
   872  
   873  	headCommitID, err := gitRepo.GetRefCommitID(pull.GetGitRefName())
   874  	if err != nil {
   875  		ctx.ServerError("GetRefCommitID", err)
   876  		return
   877  	}
   878  
   879  	ctx.Data["IsShowingOnlySingleCommit"] = willShowSpecifiedCommit
   880  
   881  	if willShowSpecifiedCommit || willShowSpecifiedCommitRange {
   882  		if len(specifiedEndCommit) > 0 {
   883  			endCommitID = specifiedEndCommit
   884  		} else {
   885  			endCommitID = headCommitID
   886  		}
   887  		if len(specifiedStartCommit) > 0 {
   888  			startCommitID = specifiedStartCommit
   889  		} else {
   890  			startCommitID = prInfo.MergeBase
   891  		}
   892  		ctx.Data["IsShowingAllCommits"] = false
   893  	} else {
   894  		endCommitID = headCommitID
   895  		startCommitID = prInfo.MergeBase
   896  		ctx.Data["IsShowingAllCommits"] = true
   897  	}
   898  
   899  	ctx.Data["Username"] = ctx.Repo.Owner.Name
   900  	ctx.Data["Reponame"] = ctx.Repo.Repository.Name
   901  	ctx.Data["AfterCommitID"] = endCommitID
   902  	ctx.Data["BeforeCommitID"] = startCommitID
   903  
   904  	fileOnly := ctx.FormBool("file-only")
   905  
   906  	maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
   907  	files := ctx.FormStrings("files")
   908  	if fileOnly && (len(files) == 2 || len(files) == 1) {
   909  		maxLines, maxFiles = -1, -1
   910  	}
   911  
   912  	diffOptions := &gitdiff.DiffOptions{
   913  		AfterCommitID:      endCommitID,
   914  		SkipTo:             ctx.FormString("skip-to"),
   915  		MaxLines:           maxLines,
   916  		MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
   917  		MaxFiles:           maxFiles,
   918  		WhitespaceBehavior: gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)),
   919  	}
   920  
   921  	if !willShowSpecifiedCommit {
   922  		diffOptions.BeforeCommitID = startCommitID
   923  	}
   924  
   925  	var methodWithError string
   926  	var diff *gitdiff.Diff
   927  
   928  	// if we're not logged in or only a single commit (or commit range) is shown we
   929  	// have to load only the diff and not get the viewed information
   930  	// as the viewed information is designed to be loaded only on latest PR
   931  	// diff and if you're signed in.
   932  	if !ctx.IsSigned || willShowSpecifiedCommit || willShowSpecifiedCommitRange {
   933  		diff, err = gitdiff.GetDiff(gitRepo, diffOptions, files...)
   934  		methodWithError = "GetDiff"
   935  	} else {
   936  		diff, err = gitdiff.SyncAndGetUserSpecificDiff(ctx, ctx.Doer.ID, pull, gitRepo, diffOptions, files...)
   937  		methodWithError = "SyncAndGetUserSpecificDiff"
   938  	}
   939  	if err != nil {
   940  		ctx.ServerError(methodWithError, err)
   941  		return
   942  	}
   943  
   944  	ctx.PageData["prReview"] = map[string]any{
   945  		"numberOfFiles":       diff.NumFiles,
   946  		"numberOfViewedFiles": diff.NumViewedFiles,
   947  	}
   948  
   949  	if err = diff.LoadComments(ctx, issue, ctx.Doer, ctx.Data["ShowOutdatedComments"].(bool)); err != nil {
   950  		ctx.ServerError("LoadComments", err)
   951  		return
   952  	}
   953  
   954  	pb, err := git_model.GetFirstMatchProtectedBranchRule(ctx, pull.BaseRepoID, pull.BaseBranch)
   955  	if err != nil {
   956  		ctx.ServerError("LoadProtectedBranch", err)
   957  		return
   958  	}
   959  
   960  	if pb != nil {
   961  		glob := pb.GetProtectedFilePatterns()
   962  		if len(glob) != 0 {
   963  			for _, file := range diff.Files {
   964  				file.IsProtected = pb.IsProtectedFile(glob, file.Name)
   965  			}
   966  		}
   967  	}
   968  
   969  	ctx.Data["Diff"] = diff
   970  	ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
   971  
   972  	baseCommit, err := ctx.Repo.GitRepo.GetCommit(startCommitID)
   973  	if err != nil {
   974  		ctx.ServerError("GetCommit", err)
   975  		return
   976  	}
   977  	commit, err := gitRepo.GetCommit(endCommitID)
   978  	if err != nil {
   979  		ctx.ServerError("GetCommit", err)
   980  		return
   981  	}
   982  
   983  	if ctx.IsSigned && ctx.Doer != nil {
   984  		if ctx.Data["CanMarkConversation"], err = issues_model.CanMarkConversation(ctx, issue, ctx.Doer); err != nil {
   985  			ctx.ServerError("CanMarkConversation", err)
   986  			return
   987  		}
   988  	}
   989  
   990  	setCompareContext(ctx, baseCommit, commit, ctx.Repo.Owner.Name, ctx.Repo.Repository.Name)
   991  
   992  	assigneeUsers, err := repo_model.GetRepoAssignees(ctx, ctx.Repo.Repository)
   993  	if err != nil {
   994  		ctx.ServerError("GetRepoAssignees", err)
   995  		return
   996  	}
   997  	ctx.Data["Assignees"] = MakeSelfOnTop(ctx.Doer, assigneeUsers)
   998  
   999  	handleTeamMentions(ctx)
  1000  	if ctx.Written() {
  1001  		return
  1002  	}
  1003  
  1004  	currentReview, err := issues_model.GetCurrentReview(ctx, ctx.Doer, issue)
  1005  	if err != nil && !issues_model.IsErrReviewNotExist(err) {
  1006  		ctx.ServerError("GetCurrentReview", err)
  1007  		return
  1008  	}
  1009  	numPendingCodeComments := int64(0)
  1010  	if currentReview != nil {
  1011  		numPendingCodeComments, err = issues_model.CountComments(ctx, &issues_model.FindCommentsOptions{
  1012  			Type:     issues_model.CommentTypeCode,
  1013  			ReviewID: currentReview.ID,
  1014  			IssueID:  issue.ID,
  1015  		})
  1016  		if err != nil {
  1017  			ctx.ServerError("CountComments", err)
  1018  			return
  1019  		}
  1020  	}
  1021  	ctx.Data["CurrentReview"] = currentReview
  1022  	ctx.Data["PendingCodeCommentNumber"] = numPendingCodeComments
  1023  
  1024  	getBranchData(ctx, issue)
  1025  	ctx.Data["IsIssuePoster"] = ctx.IsSigned && issue.IsPoster(ctx.Doer.ID)
  1026  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)
  1027  
  1028  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  1029  	// For files changed page
  1030  	PrepareBranchList(ctx)
  1031  	if ctx.Written() {
  1032  		return
  1033  	}
  1034  	upload.AddUploadContext(ctx, "comment")
  1035  
  1036  	ctx.HTML(http.StatusOK, tplPullFiles)
  1037  }
  1038  
  1039  func ViewPullFilesForSingleCommit(ctx *context.Context) {
  1040  	viewPullFiles(ctx, "", ctx.Params("sha"), true, true)
  1041  }
  1042  
  1043  func ViewPullFilesForRange(ctx *context.Context) {
  1044  	viewPullFiles(ctx, ctx.Params("shaFrom"), ctx.Params("shaTo"), true, false)
  1045  }
  1046  
  1047  func ViewPullFilesStartingFromCommit(ctx *context.Context) {
  1048  	viewPullFiles(ctx, "", ctx.Params("sha"), true, false)
  1049  }
  1050  
  1051  func ViewPullFilesForAllCommitsOfPr(ctx *context.Context) {
  1052  	viewPullFiles(ctx, "", "", false, false)
  1053  }
  1054  
  1055  // UpdatePullRequest merge PR's baseBranch into headBranch
  1056  func UpdatePullRequest(ctx *context.Context) {
  1057  	issue, ok := getPullInfo(ctx)
  1058  	if !ok {
  1059  		return
  1060  	}
  1061  	if issue.IsClosed {
  1062  		ctx.NotFound("MergePullRequest", nil)
  1063  		return
  1064  	}
  1065  	if issue.PullRequest.HasMerged {
  1066  		ctx.NotFound("MergePullRequest", nil)
  1067  		return
  1068  	}
  1069  
  1070  	rebase := ctx.FormString("style") == "rebase"
  1071  
  1072  	if err := issue.PullRequest.LoadBaseRepo(ctx); err != nil {
  1073  		ctx.ServerError("LoadBaseRepo", err)
  1074  		return
  1075  	}
  1076  	if err := issue.PullRequest.LoadHeadRepo(ctx); err != nil {
  1077  		ctx.ServerError("LoadHeadRepo", err)
  1078  		return
  1079  	}
  1080  
  1081  	allowedUpdateByMerge, allowedUpdateByRebase, err := pull_service.IsUserAllowedToUpdate(ctx, issue.PullRequest, ctx.Doer)
  1082  	if err != nil {
  1083  		ctx.ServerError("IsUserAllowedToMerge", err)
  1084  		return
  1085  	}
  1086  
  1087  	// ToDo: add check if maintainers are allowed to change branch ... (need migration & co)
  1088  	if (!allowedUpdateByMerge && !rebase) || (rebase && !allowedUpdateByRebase) {
  1089  		ctx.Flash.Error(ctx.Tr("repo.pulls.update_not_allowed"))
  1090  		ctx.Redirect(issue.Link())
  1091  		return
  1092  	}
  1093  
  1094  	// default merge commit message
  1095  	message := fmt.Sprintf("Merge branch '%s' into %s", issue.PullRequest.BaseBranch, issue.PullRequest.HeadBranch)
  1096  
  1097  	if err = pull_service.Update(ctx, issue.PullRequest, ctx.Doer, message, rebase); err != nil {
  1098  		if models.IsErrMergeConflicts(err) {
  1099  			conflictError := err.(models.ErrMergeConflicts)
  1100  			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1101  				"Message": ctx.Tr("repo.pulls.merge_conflict"),
  1102  				"Summary": ctx.Tr("repo.pulls.merge_conflict_summary"),
  1103  				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
  1104  			})
  1105  			if err != nil {
  1106  				ctx.ServerError("UpdatePullRequest.HTMLString", err)
  1107  				return
  1108  			}
  1109  			ctx.Flash.Error(flashError)
  1110  			ctx.Redirect(issue.Link())
  1111  			return
  1112  		} else if models.IsErrRebaseConflicts(err) {
  1113  			conflictError := err.(models.ErrRebaseConflicts)
  1114  			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1115  				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
  1116  				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
  1117  				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
  1118  			})
  1119  			if err != nil {
  1120  				ctx.ServerError("UpdatePullRequest.HTMLString", err)
  1121  				return
  1122  			}
  1123  			ctx.Flash.Error(flashError)
  1124  			ctx.Redirect(issue.Link())
  1125  			return
  1126  
  1127  		}
  1128  		ctx.Flash.Error(err.Error())
  1129  		ctx.Redirect(issue.Link())
  1130  		return
  1131  	}
  1132  
  1133  	time.Sleep(1 * time.Second)
  1134  
  1135  	ctx.Flash.Success(ctx.Tr("repo.pulls.update_branch_success"))
  1136  	ctx.Redirect(issue.Link())
  1137  }
  1138  
  1139  // MergePullRequest response for merging pull request
  1140  func MergePullRequest(ctx *context.Context) {
  1141  	form := web.GetForm(ctx).(*forms.MergePullRequestForm)
  1142  	issue, ok := getPullInfo(ctx)
  1143  	if !ok {
  1144  		return
  1145  	}
  1146  
  1147  	pr := issue.PullRequest
  1148  	pr.Issue = issue
  1149  	pr.Issue.Repo = ctx.Repo.Repository
  1150  
  1151  	manuallyMerged := repo_model.MergeStyle(form.Do) == repo_model.MergeStyleManuallyMerged
  1152  
  1153  	mergeCheckType := pull_service.MergeCheckTypeGeneral
  1154  	if form.MergeWhenChecksSucceed {
  1155  		mergeCheckType = pull_service.MergeCheckTypeAuto
  1156  	}
  1157  	if manuallyMerged {
  1158  		mergeCheckType = pull_service.MergeCheckTypeManually
  1159  	}
  1160  
  1161  	// start with merging by checking
  1162  	if err := pull_service.CheckPullMergable(ctx, ctx.Doer, &ctx.Repo.Permission, pr, mergeCheckType, form.ForceMerge); err != nil {
  1163  		switch {
  1164  		case errors.Is(err, pull_service.ErrIsClosed):
  1165  			if issue.IsPull {
  1166  				ctx.JSONError(ctx.Tr("repo.pulls.is_closed"))
  1167  			} else {
  1168  				ctx.JSONError(ctx.Tr("repo.issues.closed_title"))
  1169  			}
  1170  		case errors.Is(err, pull_service.ErrUserNotAllowedToMerge):
  1171  			ctx.JSONError(ctx.Tr("repo.pulls.update_not_allowed"))
  1172  		case errors.Is(err, pull_service.ErrHasMerged):
  1173  			ctx.JSONError(ctx.Tr("repo.pulls.has_merged"))
  1174  		case errors.Is(err, pull_service.ErrIsWorkInProgress):
  1175  			ctx.JSONError(ctx.Tr("repo.pulls.no_merge_wip"))
  1176  		case errors.Is(err, pull_service.ErrNotMergableState):
  1177  			ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
  1178  		case models.IsErrDisallowedToMerge(err):
  1179  			ctx.JSONError(ctx.Tr("repo.pulls.no_merge_not_ready"))
  1180  		case asymkey_service.IsErrWontSign(err):
  1181  			ctx.JSONError(err.Error()) // has no translation ...
  1182  		case errors.Is(err, pull_service.ErrDependenciesLeft):
  1183  			ctx.JSONError(ctx.Tr("repo.issues.dependency.pr_close_blocked"))
  1184  		default:
  1185  			ctx.ServerError("WebCheck", err)
  1186  		}
  1187  
  1188  		return
  1189  	}
  1190  
  1191  	// handle manually-merged mark
  1192  	if manuallyMerged {
  1193  		if err := pull_service.MergedManually(pr, ctx.Doer, ctx.Repo.GitRepo, form.MergeCommitID); err != nil {
  1194  			switch {
  1195  			case models.IsErrInvalidMergeStyle(err):
  1196  				ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
  1197  			case strings.Contains(err.Error(), "Wrong commit ID"):
  1198  				ctx.JSONError(ctx.Tr("repo.pulls.wrong_commit_id"))
  1199  			default:
  1200  				ctx.ServerError("MergedManually", err)
  1201  			}
  1202  
  1203  			return
  1204  		}
  1205  
  1206  		ctx.JSONRedirect(issue.Link())
  1207  		return
  1208  	}
  1209  
  1210  	message := strings.TrimSpace(form.MergeTitleField)
  1211  	if len(message) == 0 {
  1212  		var err error
  1213  		message, _, err = pull_service.GetDefaultMergeMessage(ctx, ctx.Repo.GitRepo, pr, repo_model.MergeStyle(form.Do))
  1214  		if err != nil {
  1215  			ctx.ServerError("GetDefaultMergeMessage", err)
  1216  			return
  1217  		}
  1218  	}
  1219  
  1220  	form.MergeMessageField = strings.TrimSpace(form.MergeMessageField)
  1221  	if len(form.MergeMessageField) > 0 {
  1222  		message += "\n\n" + form.MergeMessageField
  1223  	}
  1224  
  1225  	if form.MergeWhenChecksSucceed {
  1226  		// delete all scheduled auto merges
  1227  		_ = pull_model.DeleteScheduledAutoMerge(ctx, pr.ID)
  1228  		// schedule auto merge
  1229  		scheduled, err := automerge.ScheduleAutoMerge(ctx, ctx.Doer, pr, repo_model.MergeStyle(form.Do), message)
  1230  		if err != nil {
  1231  			ctx.ServerError("ScheduleAutoMerge", err)
  1232  			return
  1233  		} else if scheduled {
  1234  			// nothing more to do ...
  1235  			ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_newly_scheduled"))
  1236  			ctx.JSONRedirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, pr.Index))
  1237  			return
  1238  		}
  1239  	}
  1240  
  1241  	if err := pull_service.Merge(ctx, pr, ctx.Doer, ctx.Repo.GitRepo, repo_model.MergeStyle(form.Do), form.HeadCommitID, message, false); err != nil {
  1242  		if models.IsErrInvalidMergeStyle(err) {
  1243  			ctx.JSONError(ctx.Tr("repo.pulls.invalid_merge_option"))
  1244  		} else if models.IsErrMergeConflicts(err) {
  1245  			conflictError := err.(models.ErrMergeConflicts)
  1246  			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1247  				"Message": ctx.Tr("repo.editor.merge_conflict"),
  1248  				"Summary": ctx.Tr("repo.editor.merge_conflict_summary"),
  1249  				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
  1250  			})
  1251  			if err != nil {
  1252  				ctx.ServerError("MergePullRequest.HTMLString", err)
  1253  				return
  1254  			}
  1255  			ctx.Flash.Error(flashError)
  1256  			ctx.JSONRedirect(issue.Link())
  1257  		} else if models.IsErrRebaseConflicts(err) {
  1258  			conflictError := err.(models.ErrRebaseConflicts)
  1259  			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1260  				"Message": ctx.Tr("repo.pulls.rebase_conflict", utils.SanitizeFlashErrorString(conflictError.CommitSHA)),
  1261  				"Summary": ctx.Tr("repo.pulls.rebase_conflict_summary"),
  1262  				"Details": utils.SanitizeFlashErrorString(conflictError.StdErr) + "<br>" + utils.SanitizeFlashErrorString(conflictError.StdOut),
  1263  			})
  1264  			if err != nil {
  1265  				ctx.ServerError("MergePullRequest.HTMLString", err)
  1266  				return
  1267  			}
  1268  			ctx.Flash.Error(flashError)
  1269  			ctx.JSONRedirect(issue.Link())
  1270  		} else if models.IsErrMergeUnrelatedHistories(err) {
  1271  			log.Debug("MergeUnrelatedHistories error: %v", err)
  1272  			ctx.Flash.Error(ctx.Tr("repo.pulls.unrelated_histories"))
  1273  			ctx.JSONRedirect(issue.Link())
  1274  		} else if git.IsErrPushOutOfDate(err) {
  1275  			log.Debug("MergePushOutOfDate error: %v", err)
  1276  			ctx.Flash.Error(ctx.Tr("repo.pulls.merge_out_of_date"))
  1277  			ctx.JSONRedirect(issue.Link())
  1278  		} else if models.IsErrSHADoesNotMatch(err) {
  1279  			log.Debug("MergeHeadOutOfDate error: %v", err)
  1280  			ctx.Flash.Error(ctx.Tr("repo.pulls.head_out_of_date"))
  1281  			ctx.JSONRedirect(issue.Link())
  1282  		} else if git.IsErrPushRejected(err) {
  1283  			log.Debug("MergePushRejected error: %v", err)
  1284  			pushrejErr := err.(*git.ErrPushRejected)
  1285  			message := pushrejErr.Message
  1286  			if len(message) == 0 {
  1287  				ctx.Flash.Error(ctx.Tr("repo.pulls.push_rejected_no_message"))
  1288  			} else {
  1289  				flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1290  					"Message": ctx.Tr("repo.pulls.push_rejected"),
  1291  					"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
  1292  					"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
  1293  				})
  1294  				if err != nil {
  1295  					ctx.ServerError("MergePullRequest.HTMLString", err)
  1296  					return
  1297  				}
  1298  				ctx.Flash.Error(flashError)
  1299  			}
  1300  			ctx.JSONRedirect(issue.Link())
  1301  		} else {
  1302  			ctx.ServerError("Merge", err)
  1303  		}
  1304  		return
  1305  	}
  1306  	log.Trace("Pull request merged: %d", pr.ID)
  1307  
  1308  	if err := stopTimerIfAvailable(ctx, ctx.Doer, issue); err != nil {
  1309  		ctx.ServerError("stopTimerIfAvailable", err)
  1310  		return
  1311  	}
  1312  
  1313  	log.Trace("Pull request merged: %d", pr.ID)
  1314  
  1315  	if form.DeleteBranchAfterMerge {
  1316  		// Don't cleanup when other pr use this branch as head branch
  1317  		exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
  1318  		if err != nil {
  1319  			ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
  1320  			return
  1321  		}
  1322  		if exist {
  1323  			ctx.JSONRedirect(issue.Link())
  1324  			return
  1325  		}
  1326  
  1327  		var headRepo *git.Repository
  1328  		if ctx.Repo != nil && ctx.Repo.Repository != nil && pr.HeadRepoID == ctx.Repo.Repository.ID && ctx.Repo.GitRepo != nil {
  1329  			headRepo = ctx.Repo.GitRepo
  1330  		} else {
  1331  			headRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
  1332  			if err != nil {
  1333  				ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
  1334  				return
  1335  			}
  1336  			defer headRepo.Close()
  1337  		}
  1338  		deleteBranch(ctx, pr, headRepo)
  1339  	}
  1340  
  1341  	ctx.JSONRedirect(issue.Link())
  1342  }
  1343  
  1344  // CancelAutoMergePullRequest cancels a scheduled pr
  1345  func CancelAutoMergePullRequest(ctx *context.Context) {
  1346  	issue, ok := getPullInfo(ctx)
  1347  	if !ok {
  1348  		return
  1349  	}
  1350  
  1351  	if err := automerge.RemoveScheduledAutoMerge(ctx, ctx.Doer, issue.PullRequest); err != nil {
  1352  		if db.IsErrNotExist(err) {
  1353  			ctx.Flash.Error(ctx.Tr("repo.pulls.auto_merge_not_scheduled"))
  1354  			ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
  1355  			return
  1356  		}
  1357  		ctx.ServerError("RemoveScheduledAutoMerge", err)
  1358  		return
  1359  	}
  1360  	ctx.Flash.Success(ctx.Tr("repo.pulls.auto_merge_canceled_schedule"))
  1361  	ctx.Redirect(fmt.Sprintf("%s/pulls/%d", ctx.Repo.RepoLink, issue.Index))
  1362  }
  1363  
  1364  func stopTimerIfAvailable(ctx *context.Context, user *user_model.User, issue *issues_model.Issue) error {
  1365  	if issues_model.StopwatchExists(ctx, user.ID, issue.ID) {
  1366  		if err := issues_model.CreateOrStopIssueStopwatch(ctx, user, issue); err != nil {
  1367  			return err
  1368  		}
  1369  	}
  1370  
  1371  	return nil
  1372  }
  1373  
  1374  // CompareAndPullRequestPost response for creating pull request
  1375  func CompareAndPullRequestPost(ctx *context.Context) {
  1376  	form := web.GetForm(ctx).(*forms.CreateIssueForm)
  1377  	ctx.Data["Title"] = ctx.Tr("repo.pulls.compare_changes")
  1378  	ctx.Data["PageIsComparePull"] = true
  1379  	ctx.Data["IsDiffCompare"] = true
  1380  	ctx.Data["IsRepoToolbarCommits"] = true
  1381  	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
  1382  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
  1383  	upload.AddUploadContext(ctx, "comment")
  1384  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests)
  1385  
  1386  	var (
  1387  		repo        = ctx.Repo.Repository
  1388  		attachments []string
  1389  	)
  1390  
  1391  	ci := ParseCompareInfo(ctx)
  1392  	defer func() {
  1393  		if ci != nil && ci.HeadGitRepo != nil {
  1394  			ci.HeadGitRepo.Close()
  1395  		}
  1396  	}()
  1397  	if ctx.Written() {
  1398  		return
  1399  	}
  1400  
  1401  	labelIDs, assigneeIDs, milestoneID, projectID := ValidateRepoMetas(ctx, *form, true)
  1402  	if ctx.Written() {
  1403  		return
  1404  	}
  1405  
  1406  	if setting.Attachment.Enabled {
  1407  		attachments = form.Files
  1408  	}
  1409  
  1410  	if ctx.HasError() {
  1411  		ctx.JSONError(ctx.GetErrMsg())
  1412  		return
  1413  	}
  1414  
  1415  	if util.IsEmptyString(form.Title) {
  1416  		ctx.JSONError(ctx.Tr("repo.issues.new.title_empty"))
  1417  		return
  1418  	}
  1419  
  1420  	content := form.Content
  1421  	if filename := ctx.Req.Form.Get("template-file"); filename != "" {
  1422  		if template, err := issue_template.UnmarshalFromRepo(ctx.Repo.GitRepo, ctx.Repo.Repository.DefaultBranch, filename); err == nil {
  1423  			content = issue_template.RenderToMarkdown(template, ctx.Req.Form)
  1424  		}
  1425  	}
  1426  
  1427  	pullIssue := &issues_model.Issue{
  1428  		RepoID:      repo.ID,
  1429  		Repo:        repo,
  1430  		Title:       form.Title,
  1431  		PosterID:    ctx.Doer.ID,
  1432  		Poster:      ctx.Doer,
  1433  		MilestoneID: milestoneID,
  1434  		IsPull:      true,
  1435  		Content:     content,
  1436  	}
  1437  	pullRequest := &issues_model.PullRequest{
  1438  		HeadRepoID:          ci.HeadRepo.ID,
  1439  		BaseRepoID:          repo.ID,
  1440  		HeadBranch:          ci.HeadBranch,
  1441  		BaseBranch:          ci.BaseBranch,
  1442  		HeadRepo:            ci.HeadRepo,
  1443  		BaseRepo:            repo,
  1444  		MergeBase:           ci.CompareInfo.MergeBase,
  1445  		Type:                issues_model.PullRequestGitea,
  1446  		AllowMaintainerEdit: form.AllowMaintainerEdit,
  1447  	}
  1448  	// FIXME: check error in the case two people send pull request at almost same time, give nice error prompt
  1449  	// instead of 500.
  1450  
  1451  	if err := pull_service.NewPullRequest(ctx, repo, pullIssue, labelIDs, attachments, pullRequest, assigneeIDs); err != nil {
  1452  		if repo_model.IsErrUserDoesNotHaveAccessToRepo(err) {
  1453  			ctx.Error(http.StatusBadRequest, "UserDoesNotHaveAccessToRepo", err.Error())
  1454  			return
  1455  		} else if git.IsErrPushRejected(err) {
  1456  			pushrejErr := err.(*git.ErrPushRejected)
  1457  			message := pushrejErr.Message
  1458  			if len(message) == 0 {
  1459  				ctx.JSONError(ctx.Tr("repo.pulls.push_rejected_no_message"))
  1460  				return
  1461  			}
  1462  			flashError, err := ctx.RenderToString(tplAlertDetails, map[string]any{
  1463  				"Message": ctx.Tr("repo.pulls.push_rejected"),
  1464  				"Summary": ctx.Tr("repo.pulls.push_rejected_summary"),
  1465  				"Details": utils.SanitizeFlashErrorString(pushrejErr.Message),
  1466  			})
  1467  			if err != nil {
  1468  				ctx.ServerError("CompareAndPullRequest.HTMLString", err)
  1469  				return
  1470  			}
  1471  			ctx.Flash.Error(flashError)
  1472  			ctx.JSONRedirect(pullIssue.Link()) // FIXME: it's unfriendly, and will make the content lost
  1473  			return
  1474  		}
  1475  		ctx.ServerError("NewPullRequest", err)
  1476  		return
  1477  	}
  1478  
  1479  	if projectID > 0 {
  1480  		if !ctx.Repo.CanWrite(unit.TypeProjects) {
  1481  			ctx.Error(http.StatusBadRequest, "user hasn't the permission to write to projects")
  1482  			return
  1483  		}
  1484  		if err := issues_model.ChangeProjectAssign(pullIssue, ctx.Doer, projectID); err != nil {
  1485  			ctx.ServerError("ChangeProjectAssign", err)
  1486  			return
  1487  		}
  1488  	}
  1489  
  1490  	log.Trace("Pull request created: %d/%d", repo.ID, pullIssue.ID)
  1491  	ctx.JSONRedirect(pullIssue.Link())
  1492  }
  1493  
  1494  // CleanUpPullRequest responses for delete merged branch when PR has been merged
  1495  func CleanUpPullRequest(ctx *context.Context) {
  1496  	issue, ok := getPullInfo(ctx)
  1497  	if !ok {
  1498  		return
  1499  	}
  1500  
  1501  	pr := issue.PullRequest
  1502  
  1503  	// Don't cleanup unmerged and unclosed PRs
  1504  	if !pr.HasMerged && !issue.IsClosed {
  1505  		ctx.NotFound("CleanUpPullRequest", nil)
  1506  		return
  1507  	}
  1508  
  1509  	// Don't cleanup when there are other PR's that use this branch as head branch.
  1510  	exist, err := issues_model.HasUnmergedPullRequestsByHeadInfo(ctx, pr.HeadRepoID, pr.HeadBranch)
  1511  	if err != nil {
  1512  		ctx.ServerError("HasUnmergedPullRequestsByHeadInfo", err)
  1513  		return
  1514  	}
  1515  	if exist {
  1516  		ctx.NotFound("CleanUpPullRequest", nil)
  1517  		return
  1518  	}
  1519  
  1520  	if err := pr.LoadHeadRepo(ctx); err != nil {
  1521  		ctx.ServerError("LoadHeadRepo", err)
  1522  		return
  1523  	} else if pr.HeadRepo == nil {
  1524  		// Forked repository has already been deleted
  1525  		ctx.NotFound("CleanUpPullRequest", nil)
  1526  		return
  1527  	} else if err = pr.LoadBaseRepo(ctx); err != nil {
  1528  		ctx.ServerError("LoadBaseRepo", err)
  1529  		return
  1530  	} else if err = pr.HeadRepo.LoadOwner(ctx); err != nil {
  1531  		ctx.ServerError("HeadRepo.LoadOwner", err)
  1532  		return
  1533  	}
  1534  
  1535  	perm, err := access_model.GetUserRepoPermission(ctx, pr.HeadRepo, ctx.Doer)
  1536  	if err != nil {
  1537  		ctx.ServerError("GetUserRepoPermission", err)
  1538  		return
  1539  	}
  1540  	if !perm.CanWrite(unit.TypeCode) {
  1541  		ctx.NotFound("CleanUpPullRequest", nil)
  1542  		return
  1543  	}
  1544  
  1545  	fullBranchName := pr.HeadRepo.Owner.Name + "/" + pr.HeadBranch
  1546  
  1547  	var gitBaseRepo *git.Repository
  1548  
  1549  	// Assume that the base repo is the current context (almost certainly)
  1550  	if ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.BaseRepoID && ctx.Repo.GitRepo != nil {
  1551  		gitBaseRepo = ctx.Repo.GitRepo
  1552  	} else {
  1553  		// If not just open it
  1554  		gitBaseRepo, err = git.OpenRepository(ctx, pr.BaseRepo.RepoPath())
  1555  		if err != nil {
  1556  			ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.BaseRepo.RepoPath()), err)
  1557  			return
  1558  		}
  1559  		defer gitBaseRepo.Close()
  1560  	}
  1561  
  1562  	// Now assume that the head repo is the same as the base repo (reasonable chance)
  1563  	gitRepo := gitBaseRepo
  1564  	// But if not: is it the same as the context?
  1565  	if pr.BaseRepoID != pr.HeadRepoID && ctx.Repo != nil && ctx.Repo.Repository != nil && ctx.Repo.Repository.ID == pr.HeadRepoID && ctx.Repo.GitRepo != nil {
  1566  		gitRepo = ctx.Repo.GitRepo
  1567  	} else if pr.BaseRepoID != pr.HeadRepoID {
  1568  		// Otherwise just load it up
  1569  		gitRepo, err = git.OpenRepository(ctx, pr.HeadRepo.RepoPath())
  1570  		if err != nil {
  1571  			ctx.ServerError(fmt.Sprintf("OpenRepository[%s]", pr.HeadRepo.RepoPath()), err)
  1572  			return
  1573  		}
  1574  		defer gitRepo.Close()
  1575  	}
  1576  
  1577  	defer func() {
  1578  		ctx.JSONRedirect(issue.Link())
  1579  	}()
  1580  
  1581  	// Check if branch has no new commits
  1582  	headCommitID, err := gitBaseRepo.GetRefCommitID(pr.GetGitRefName())
  1583  	if err != nil {
  1584  		log.Error("GetRefCommitID: %v", err)
  1585  		ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1586  		return
  1587  	}
  1588  	branchCommitID, err := gitRepo.GetBranchCommitID(pr.HeadBranch)
  1589  	if err != nil {
  1590  		log.Error("GetBranchCommitID: %v", err)
  1591  		ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1592  		return
  1593  	}
  1594  	if headCommitID != branchCommitID {
  1595  		ctx.Flash.Error(ctx.Tr("repo.branch.delete_branch_has_new_commits", fullBranchName))
  1596  		return
  1597  	}
  1598  
  1599  	deleteBranch(ctx, pr, gitRepo)
  1600  }
  1601  
  1602  func deleteBranch(ctx *context.Context, pr *issues_model.PullRequest, gitRepo *git.Repository) {
  1603  	fullBranchName := pr.HeadRepo.FullName() + ":" + pr.HeadBranch
  1604  	if err := repo_service.DeleteBranch(ctx, ctx.Doer, pr.HeadRepo, gitRepo, pr.HeadBranch); err != nil {
  1605  		switch {
  1606  		case git.IsErrBranchNotExist(err):
  1607  			ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1608  		case errors.Is(err, repo_service.ErrBranchIsDefault):
  1609  			ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1610  		case errors.Is(err, git_model.ErrBranchIsProtected):
  1611  			ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1612  		default:
  1613  			log.Error("DeleteBranch: %v", err)
  1614  			ctx.Flash.Error(ctx.Tr("repo.branch.deletion_failed", fullBranchName))
  1615  		}
  1616  		return
  1617  	}
  1618  
  1619  	if err := issues_model.AddDeletePRBranchComment(ctx, ctx.Doer, pr.BaseRepo, pr.IssueID, pr.HeadBranch); err != nil {
  1620  		// Do not fail here as branch has already been deleted
  1621  		log.Error("DeleteBranch: %v", err)
  1622  	}
  1623  
  1624  	ctx.Flash.Success(ctx.Tr("repo.branch.deletion_success", fullBranchName))
  1625  }
  1626  
  1627  // DownloadPullDiff render a pull's raw diff
  1628  func DownloadPullDiff(ctx *context.Context) {
  1629  	DownloadPullDiffOrPatch(ctx, false)
  1630  }
  1631  
  1632  // DownloadPullPatch render a pull's raw patch
  1633  func DownloadPullPatch(ctx *context.Context) {
  1634  	DownloadPullDiffOrPatch(ctx, true)
  1635  }
  1636  
  1637  // DownloadPullDiffOrPatch render a pull's raw diff or patch
  1638  func DownloadPullDiffOrPatch(ctx *context.Context, patch bool) {
  1639  	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1640  	if err != nil {
  1641  		if issues_model.IsErrPullRequestNotExist(err) {
  1642  			ctx.NotFound("GetPullRequestByIndex", err)
  1643  		} else {
  1644  			ctx.ServerError("GetPullRequestByIndex", err)
  1645  		}
  1646  		return
  1647  	}
  1648  
  1649  	binary := ctx.FormBool("binary")
  1650  
  1651  	if err := pull_service.DownloadDiffOrPatch(ctx, pr, ctx, patch, binary); err != nil {
  1652  		ctx.ServerError("DownloadDiffOrPatch", err)
  1653  		return
  1654  	}
  1655  }
  1656  
  1657  // UpdatePullRequestTarget change pull request's target branch
  1658  func UpdatePullRequestTarget(ctx *context.Context) {
  1659  	issue := GetActionIssue(ctx)
  1660  	if ctx.Written() {
  1661  		return
  1662  	}
  1663  	pr := issue.PullRequest
  1664  	if !issue.IsPull {
  1665  		ctx.Error(http.StatusNotFound)
  1666  		return
  1667  	}
  1668  
  1669  	if !ctx.IsSigned || (!issue.IsPoster(ctx.Doer.ID) && !ctx.Repo.CanWriteIssuesOrPulls(issue.IsPull)) {
  1670  		ctx.Error(http.StatusForbidden)
  1671  		return
  1672  	}
  1673  
  1674  	targetBranch := ctx.FormTrim("target_branch")
  1675  	if len(targetBranch) == 0 {
  1676  		ctx.Error(http.StatusNoContent)
  1677  		return
  1678  	}
  1679  
  1680  	if err := pull_service.ChangeTargetBranch(ctx, pr, ctx.Doer, targetBranch); err != nil {
  1681  		if issues_model.IsErrPullRequestAlreadyExists(err) {
  1682  			err := err.(issues_model.ErrPullRequestAlreadyExists)
  1683  
  1684  			RepoRelPath := ctx.Repo.Owner.Name + "/" + ctx.Repo.Repository.Name
  1685  			errorMessage := ctx.Tr("repo.pulls.has_pull_request", html.EscapeString(ctx.Repo.RepoLink+"/pulls/"+strconv.FormatInt(err.IssueID, 10)), html.EscapeString(RepoRelPath), err.IssueID) // FIXME: Creates url inside locale string
  1686  
  1687  			ctx.Flash.Error(errorMessage)
  1688  			ctx.JSON(http.StatusConflict, map[string]any{
  1689  				"error":      err.Error(),
  1690  				"user_error": errorMessage,
  1691  			})
  1692  		} else if issues_model.IsErrIssueIsClosed(err) {
  1693  			errorMessage := ctx.Tr("repo.pulls.is_closed")
  1694  
  1695  			ctx.Flash.Error(errorMessage)
  1696  			ctx.JSON(http.StatusConflict, map[string]any{
  1697  				"error":      err.Error(),
  1698  				"user_error": errorMessage,
  1699  			})
  1700  		} else if models.IsErrPullRequestHasMerged(err) {
  1701  			errorMessage := ctx.Tr("repo.pulls.has_merged")
  1702  
  1703  			ctx.Flash.Error(errorMessage)
  1704  			ctx.JSON(http.StatusConflict, map[string]any{
  1705  				"error":      err.Error(),
  1706  				"user_error": errorMessage,
  1707  			})
  1708  		} else if git_model.IsErrBranchesEqual(err) {
  1709  			errorMessage := ctx.Tr("repo.pulls.nothing_to_compare")
  1710  
  1711  			ctx.Flash.Error(errorMessage)
  1712  			ctx.JSON(http.StatusBadRequest, map[string]any{
  1713  				"error":      err.Error(),
  1714  				"user_error": errorMessage,
  1715  			})
  1716  		} else {
  1717  			ctx.ServerError("UpdatePullRequestTarget", err)
  1718  		}
  1719  		return
  1720  	}
  1721  	notify_service.PullRequestChangeTargetBranch(ctx, ctx.Doer, pr, targetBranch)
  1722  
  1723  	ctx.JSON(http.StatusOK, map[string]any{
  1724  		"base_branch": pr.BaseBranch,
  1725  	})
  1726  }
  1727  
  1728  // SetAllowEdits allow edits from maintainers to PRs
  1729  func SetAllowEdits(ctx *context.Context) {
  1730  	form := web.GetForm(ctx).(*forms.UpdateAllowEditsForm)
  1731  
  1732  	pr, err := issues_model.GetPullRequestByIndex(ctx, ctx.Repo.Repository.ID, ctx.ParamsInt64(":index"))
  1733  	if err != nil {
  1734  		if issues_model.IsErrPullRequestNotExist(err) {
  1735  			ctx.NotFound("GetPullRequestByIndex", err)
  1736  		} else {
  1737  			ctx.ServerError("GetPullRequestByIndex", err)
  1738  		}
  1739  		return
  1740  	}
  1741  
  1742  	if err := pull_service.SetAllowEdits(ctx, ctx.Doer, pr, form.AllowMaintainerEdit); err != nil {
  1743  		if errors.Is(pull_service.ErrUserHasNoPermissionForAction, err) {
  1744  			ctx.Error(http.StatusForbidden)
  1745  			return
  1746  		}
  1747  		ctx.ServerError("SetAllowEdits", err)
  1748  		return
  1749  	}
  1750  
  1751  	ctx.JSON(http.StatusOK, map[string]any{
  1752  		"allow_maintainer_edit": pr.AllowMaintainerEdit,
  1753  	})
  1754  }