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