code.gitea.io/gitea@v1.22.3/routers/web/repo/compare.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package repo
     5  
     6  import (
     7  	"bufio"
     8  	gocontext "context"
     9  	"encoding/csv"
    10  	"errors"
    11  	"fmt"
    12  	"html"
    13  	"io"
    14  	"net/http"
    15  	"net/url"
    16  	"path/filepath"
    17  	"strings"
    18  
    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  	repo_model "code.gitea.io/gitea/models/repo"
    24  	"code.gitea.io/gitea/models/unit"
    25  	user_model "code.gitea.io/gitea/models/user"
    26  	"code.gitea.io/gitea/modules/base"
    27  	"code.gitea.io/gitea/modules/charset"
    28  	csv_module "code.gitea.io/gitea/modules/csv"
    29  	"code.gitea.io/gitea/modules/git"
    30  	"code.gitea.io/gitea/modules/gitrepo"
    31  	"code.gitea.io/gitea/modules/log"
    32  	"code.gitea.io/gitea/modules/markup"
    33  	"code.gitea.io/gitea/modules/optional"
    34  	"code.gitea.io/gitea/modules/setting"
    35  	api "code.gitea.io/gitea/modules/structs"
    36  	"code.gitea.io/gitea/modules/typesniffer"
    37  	"code.gitea.io/gitea/modules/util"
    38  	"code.gitea.io/gitea/routers/common"
    39  	"code.gitea.io/gitea/services/context"
    40  	"code.gitea.io/gitea/services/context/upload"
    41  	"code.gitea.io/gitea/services/gitdiff"
    42  )
    43  
    44  const (
    45  	tplCompare     base.TplName = "repo/diff/compare"
    46  	tplBlobExcerpt base.TplName = "repo/diff/blob_excerpt"
    47  	tplDiffBox     base.TplName = "repo/diff/box"
    48  )
    49  
    50  // setCompareContext sets context data.
    51  func setCompareContext(ctx *context.Context, before, head *git.Commit, headOwner, headName string) {
    52  	ctx.Data["BeforeCommit"] = before
    53  	ctx.Data["HeadCommit"] = head
    54  
    55  	ctx.Data["GetBlobByPathForCommit"] = func(commit *git.Commit, path string) *git.Blob {
    56  		if commit == nil {
    57  			return nil
    58  		}
    59  
    60  		blob, err := commit.GetBlobByPath(path)
    61  		if err != nil {
    62  			return nil
    63  		}
    64  		return blob
    65  	}
    66  
    67  	ctx.Data["GetSniffedTypeForBlob"] = func(blob *git.Blob) typesniffer.SniffedType {
    68  		st := typesniffer.SniffedType{}
    69  
    70  		if blob == nil {
    71  			return st
    72  		}
    73  
    74  		st, err := blob.GuessContentType()
    75  		if err != nil {
    76  			log.Error("GuessContentType failed: %v", err)
    77  			return st
    78  		}
    79  		return st
    80  	}
    81  
    82  	setPathsCompareContext(ctx, before, head, headOwner, headName)
    83  	setImageCompareContext(ctx)
    84  	setCsvCompareContext(ctx)
    85  }
    86  
    87  // SourceCommitURL creates a relative URL for a commit in the given repository
    88  func SourceCommitURL(owner, name string, commit *git.Commit) string {
    89  	return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/src/commit/" + url.PathEscape(commit.ID.String())
    90  }
    91  
    92  // RawCommitURL creates a relative URL for the raw commit in the given repository
    93  func RawCommitURL(owner, name string, commit *git.Commit) string {
    94  	return setting.AppSubURL + "/" + url.PathEscape(owner) + "/" + url.PathEscape(name) + "/raw/commit/" + url.PathEscape(commit.ID.String())
    95  }
    96  
    97  // setPathsCompareContext sets context data for source and raw paths
    98  func setPathsCompareContext(ctx *context.Context, base, head *git.Commit, headOwner, headName string) {
    99  	ctx.Data["SourcePath"] = SourceCommitURL(headOwner, headName, head)
   100  	ctx.Data["RawPath"] = RawCommitURL(headOwner, headName, head)
   101  	if base != nil {
   102  		ctx.Data["BeforeSourcePath"] = SourceCommitURL(headOwner, headName, base)
   103  		ctx.Data["BeforeRawPath"] = RawCommitURL(headOwner, headName, base)
   104  	}
   105  }
   106  
   107  // setImageCompareContext sets context data that is required by image compare template
   108  func setImageCompareContext(ctx *context.Context) {
   109  	ctx.Data["IsSniffedTypeAnImage"] = func(st typesniffer.SniffedType) bool {
   110  		return st.IsImage() && (setting.UI.SVG.Enabled || !st.IsSvgImage())
   111  	}
   112  }
   113  
   114  // setCsvCompareContext sets context data that is required by the CSV compare template
   115  func setCsvCompareContext(ctx *context.Context) {
   116  	ctx.Data["IsCsvFile"] = func(diffFile *gitdiff.DiffFile) bool {
   117  		extension := strings.ToLower(filepath.Ext(diffFile.Name))
   118  		return extension == ".csv" || extension == ".tsv"
   119  	}
   120  
   121  	type CsvDiffResult struct {
   122  		Sections []*gitdiff.TableDiffSection
   123  		Error    string
   124  	}
   125  
   126  	ctx.Data["CreateCsvDiff"] = func(diffFile *gitdiff.DiffFile, baseBlob, headBlob *git.Blob) CsvDiffResult {
   127  		if diffFile == nil {
   128  			return CsvDiffResult{nil, ""}
   129  		}
   130  
   131  		errTooLarge := errors.New(ctx.Locale.TrString("repo.error.csv.too_large"))
   132  
   133  		csvReaderFromCommit := func(ctx *markup.RenderContext, blob *git.Blob) (*csv.Reader, io.Closer, error) {
   134  			if blob == nil {
   135  				// It's ok for blob to be nil (file added or deleted)
   136  				return nil, nil, nil
   137  			}
   138  
   139  			if setting.UI.CSV.MaxFileSize != 0 && setting.UI.CSV.MaxFileSize < blob.Size() {
   140  				return nil, nil, errTooLarge
   141  			}
   142  
   143  			reader, err := blob.DataAsync()
   144  			if err != nil {
   145  				return nil, nil, err
   146  			}
   147  
   148  			csvReader, err := csv_module.CreateReaderAndDetermineDelimiter(ctx, charset.ToUTF8WithFallbackReader(reader, charset.ConvertOpts{}))
   149  			return csvReader, reader, err
   150  		}
   151  
   152  		baseReader, baseBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.OldName}, baseBlob)
   153  		if baseBlobCloser != nil {
   154  			defer baseBlobCloser.Close()
   155  		}
   156  		if err != nil {
   157  			if err == errTooLarge {
   158  				return CsvDiffResult{nil, err.Error()}
   159  			}
   160  			log.Error("error whilst creating csv.Reader from file %s in base commit %s in %s: %v", diffFile.Name, baseBlob.ID.String(), ctx.Repo.Repository.Name, err)
   161  			return CsvDiffResult{nil, "unable to load file"}
   162  		}
   163  
   164  		headReader, headBlobCloser, err := csvReaderFromCommit(&markup.RenderContext{Ctx: ctx, RelativePath: diffFile.Name}, headBlob)
   165  		if headBlobCloser != nil {
   166  			defer headBlobCloser.Close()
   167  		}
   168  		if err != nil {
   169  			if err == errTooLarge {
   170  				return CsvDiffResult{nil, err.Error()}
   171  			}
   172  			log.Error("error whilst creating csv.Reader from file %s in head commit %s in %s: %v", diffFile.Name, headBlob.ID.String(), ctx.Repo.Repository.Name, err)
   173  			return CsvDiffResult{nil, "unable to load file"}
   174  		}
   175  
   176  		sections, err := gitdiff.CreateCsvDiff(diffFile, baseReader, headReader)
   177  		if err != nil {
   178  			errMessage, err := csv_module.FormatError(err, ctx.Locale)
   179  			if err != nil {
   180  				log.Error("CreateCsvDiff FormatError failed: %v", err)
   181  				return CsvDiffResult{nil, "unknown csv diff error"}
   182  			}
   183  			return CsvDiffResult{nil, errMessage}
   184  		}
   185  		return CsvDiffResult{sections, ""}
   186  	}
   187  }
   188  
   189  // ParseCompareInfo parse compare info between two commit for preparing comparing references
   190  func ParseCompareInfo(ctx *context.Context) *common.CompareInfo {
   191  	baseRepo := ctx.Repo.Repository
   192  	ci := &common.CompareInfo{}
   193  
   194  	fileOnly := ctx.FormBool("file-only")
   195  
   196  	// Get compared branches information
   197  	// A full compare url is of the form:
   198  	//
   199  	// 1. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headBranch}
   200  	// 2. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}:{:headBranch}
   201  	// 3. /{:baseOwner}/{:baseRepoName}/compare/{:baseBranch}...{:headOwner}/{:headRepoName}:{:headBranch}
   202  	// 4. /{:baseOwner}/{:baseRepoName}/compare/{:headBranch}
   203  	// 5. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}:{:headBranch}
   204  	// 6. /{:baseOwner}/{:baseRepoName}/compare/{:headOwner}/{:headRepoName}:{:headBranch}
   205  	//
   206  	// Here we obtain the infoPath "{:baseBranch}...[{:headOwner}/{:headRepoName}:]{:headBranch}" as ctx.Params("*")
   207  	// with the :baseRepo in ctx.Repo.
   208  	//
   209  	// Note: Generally :headRepoName is not provided here - we are only passed :headOwner.
   210  	//
   211  	// How do we determine the :headRepo?
   212  	//
   213  	// 1. If :headOwner is not set then the :headRepo = :baseRepo
   214  	// 2. If :headOwner is set - then look for the fork of :baseRepo owned by :headOwner
   215  	// 3. But... :baseRepo could be a fork of :headOwner's repo - so check that
   216  	// 4. Now, :baseRepo and :headRepos could be forks of the same repo - so check that
   217  	//
   218  	// format: <base branch>...[<head repo>:]<head branch>
   219  	// base<-head: master...head:feature
   220  	// same repo: master...feature
   221  
   222  	var (
   223  		isSameRepo bool
   224  		infoPath   string
   225  		err        error
   226  	)
   227  
   228  	infoPath = ctx.Params("*")
   229  	var infos []string
   230  	if infoPath == "" {
   231  		infos = []string{baseRepo.DefaultBranch, baseRepo.DefaultBranch}
   232  	} else {
   233  		infos = strings.SplitN(infoPath, "...", 2)
   234  		if len(infos) != 2 {
   235  			if infos = strings.SplitN(infoPath, "..", 2); len(infos) == 2 {
   236  				ci.DirectComparison = true
   237  				ctx.Data["PageIsComparePull"] = false
   238  			} else {
   239  				infos = []string{baseRepo.DefaultBranch, infoPath}
   240  			}
   241  		}
   242  	}
   243  
   244  	ctx.Data["BaseName"] = baseRepo.OwnerName
   245  	ci.BaseBranch = infos[0]
   246  	ctx.Data["BaseBranch"] = ci.BaseBranch
   247  
   248  	// If there is no head repository, it means compare between same repository.
   249  	headInfos := strings.Split(infos[1], ":")
   250  	if len(headInfos) == 1 {
   251  		isSameRepo = true
   252  		ci.HeadUser = ctx.Repo.Owner
   253  		ci.HeadBranch = headInfos[0]
   254  	} else if len(headInfos) == 2 {
   255  		headInfosSplit := strings.Split(headInfos[0], "/")
   256  		if len(headInfosSplit) == 1 {
   257  			ci.HeadUser, err = user_model.GetUserByName(ctx, headInfos[0])
   258  			if err != nil {
   259  				if user_model.IsErrUserNotExist(err) {
   260  					ctx.NotFound("GetUserByName", nil)
   261  				} else {
   262  					ctx.ServerError("GetUserByName", err)
   263  				}
   264  				return nil
   265  			}
   266  			ci.HeadBranch = headInfos[1]
   267  			isSameRepo = ci.HeadUser.ID == ctx.Repo.Owner.ID
   268  			if isSameRepo {
   269  				ci.HeadRepo = baseRepo
   270  			}
   271  		} else {
   272  			ci.HeadRepo, err = repo_model.GetRepositoryByOwnerAndName(ctx, headInfosSplit[0], headInfosSplit[1])
   273  			if err != nil {
   274  				if repo_model.IsErrRepoNotExist(err) {
   275  					ctx.NotFound("GetRepositoryByOwnerAndName", nil)
   276  				} else {
   277  					ctx.ServerError("GetRepositoryByOwnerAndName", err)
   278  				}
   279  				return nil
   280  			}
   281  			if err := ci.HeadRepo.LoadOwner(ctx); err != nil {
   282  				if user_model.IsErrUserNotExist(err) {
   283  					ctx.NotFound("GetUserByName", nil)
   284  				} else {
   285  					ctx.ServerError("GetUserByName", err)
   286  				}
   287  				return nil
   288  			}
   289  			ci.HeadBranch = headInfos[1]
   290  			ci.HeadUser = ci.HeadRepo.Owner
   291  			isSameRepo = ci.HeadRepo.ID == ctx.Repo.Repository.ID
   292  		}
   293  	} else {
   294  		ctx.NotFound("CompareAndPullRequest", nil)
   295  		return nil
   296  	}
   297  	ctx.Data["HeadUser"] = ci.HeadUser
   298  	ctx.Data["HeadBranch"] = ci.HeadBranch
   299  	ctx.Repo.PullRequest.SameRepo = isSameRepo
   300  
   301  	// Check if base branch is valid.
   302  	baseIsCommit := ctx.Repo.GitRepo.IsCommitExist(ci.BaseBranch)
   303  	baseIsBranch := ctx.Repo.GitRepo.IsBranchExist(ci.BaseBranch)
   304  	baseIsTag := ctx.Repo.GitRepo.IsTagExist(ci.BaseBranch)
   305  
   306  	if !baseIsCommit && !baseIsBranch && !baseIsTag {
   307  		// Check if baseBranch is short sha commit hash
   308  		if baseCommit, _ := ctx.Repo.GitRepo.GetCommit(ci.BaseBranch); baseCommit != nil {
   309  			ci.BaseBranch = baseCommit.ID.String()
   310  			ctx.Data["BaseBranch"] = ci.BaseBranch
   311  			baseIsCommit = true
   312  		} else if ci.BaseBranch == ctx.Repo.GetObjectFormat().EmptyObjectID().String() {
   313  			if isSameRepo {
   314  				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadBranch))
   315  			} else {
   316  				ctx.Redirect(ctx.Repo.RepoLink + "/compare/" + util.PathEscapeSegments(ci.HeadRepo.FullName()) + ":" + util.PathEscapeSegments(ci.HeadBranch))
   317  			}
   318  			return nil
   319  		} else {
   320  			ctx.NotFound("IsRefExist", nil)
   321  			return nil
   322  		}
   323  	}
   324  	ctx.Data["BaseIsCommit"] = baseIsCommit
   325  	ctx.Data["BaseIsBranch"] = baseIsBranch
   326  	ctx.Data["BaseIsTag"] = baseIsTag
   327  	ctx.Data["IsPull"] = true
   328  
   329  	// Now we have the repository that represents the base
   330  
   331  	// The current base and head repositories and branches may not
   332  	// actually be the intended branches that the user wants to
   333  	// create a pull-request from - but also determining the head
   334  	// repo is difficult.
   335  
   336  	// We will want therefore to offer a few repositories to set as
   337  	// our base and head
   338  
   339  	// 1. First if the baseRepo is a fork get the "RootRepo" it was
   340  	// forked from
   341  	var rootRepo *repo_model.Repository
   342  	if baseRepo.IsFork {
   343  		err = baseRepo.GetBaseRepo(ctx)
   344  		if err != nil {
   345  			if !repo_model.IsErrRepoNotExist(err) {
   346  				ctx.ServerError("Unable to find root repo", err)
   347  				return nil
   348  			}
   349  		} else {
   350  			rootRepo = baseRepo.BaseRepo
   351  		}
   352  	}
   353  
   354  	// 2. Now if the current user is not the owner of the baseRepo,
   355  	// check if they have a fork of the base repo and offer that as
   356  	// "OwnForkRepo"
   357  	var ownForkRepo *repo_model.Repository
   358  	if ctx.Doer != nil && baseRepo.OwnerID != ctx.Doer.ID {
   359  		repo := repo_model.GetForkedRepo(ctx, ctx.Doer.ID, baseRepo.ID)
   360  		if repo != nil {
   361  			ownForkRepo = repo
   362  			ctx.Data["OwnForkRepo"] = ownForkRepo
   363  		}
   364  	}
   365  
   366  	has := ci.HeadRepo != nil
   367  	// 3. If the base is a forked from "RootRepo" and the owner of
   368  	// the "RootRepo" is the :headUser - set headRepo to that
   369  	if !has && rootRepo != nil && rootRepo.OwnerID == ci.HeadUser.ID {
   370  		ci.HeadRepo = rootRepo
   371  		has = true
   372  	}
   373  
   374  	// 4. If the ctx.Doer has their own fork of the baseRepo and the headUser is the ctx.Doer
   375  	// set the headRepo to the ownFork
   376  	if !has && ownForkRepo != nil && ownForkRepo.OwnerID == ci.HeadUser.ID {
   377  		ci.HeadRepo = ownForkRepo
   378  		has = true
   379  	}
   380  
   381  	// 5. If the headOwner has a fork of the baseRepo - use that
   382  	if !has {
   383  		ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ID)
   384  		has = ci.HeadRepo != nil
   385  	}
   386  
   387  	// 6. If the baseRepo is a fork and the headUser has a fork of that use that
   388  	if !has && baseRepo.IsFork {
   389  		ci.HeadRepo = repo_model.GetForkedRepo(ctx, ci.HeadUser.ID, baseRepo.ForkID)
   390  		has = ci.HeadRepo != nil
   391  	}
   392  
   393  	// 7. Otherwise if we're not the same repo and haven't found a repo give up
   394  	if !isSameRepo && !has {
   395  		ctx.Data["PageIsComparePull"] = false
   396  	}
   397  
   398  	// 8. Finally open the git repo
   399  	if isSameRepo {
   400  		ci.HeadRepo = ctx.Repo.Repository
   401  		ci.HeadGitRepo = ctx.Repo.GitRepo
   402  	} else if has {
   403  		ci.HeadGitRepo, err = gitrepo.OpenRepository(ctx, ci.HeadRepo)
   404  		if err != nil {
   405  			ctx.ServerError("OpenRepository", err)
   406  			return nil
   407  		}
   408  		defer ci.HeadGitRepo.Close()
   409  	} else {
   410  		ctx.NotFound("ParseCompareInfo", nil)
   411  		return nil
   412  	}
   413  
   414  	ctx.Data["HeadRepo"] = ci.HeadRepo
   415  	ctx.Data["BaseCompareRepo"] = ctx.Repo.Repository
   416  
   417  	// Now we need to assert that the ctx.Doer has permission to read
   418  	// the baseRepo's code and pulls
   419  	// (NOT headRepo's)
   420  	permBase, err := access_model.GetUserRepoPermission(ctx, baseRepo, ctx.Doer)
   421  	if err != nil {
   422  		ctx.ServerError("GetUserRepoPermission", err)
   423  		return nil
   424  	}
   425  	if !permBase.CanRead(unit.TypeCode) {
   426  		if log.IsTrace() {
   427  			log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
   428  				ctx.Doer,
   429  				baseRepo,
   430  				permBase)
   431  		}
   432  		ctx.NotFound("ParseCompareInfo", nil)
   433  		return nil
   434  	}
   435  
   436  	// If we're not merging from the same repo:
   437  	if !isSameRepo {
   438  		// Assert ctx.Doer has permission to read headRepo's codes
   439  		permHead, err := access_model.GetUserRepoPermission(ctx, ci.HeadRepo, ctx.Doer)
   440  		if err != nil {
   441  			ctx.ServerError("GetUserRepoPermission", err)
   442  			return nil
   443  		}
   444  		if !permHead.CanRead(unit.TypeCode) {
   445  			if log.IsTrace() {
   446  				log.Trace("Permission Denied: User: %-v cannot read code in Repo: %-v\nUser in headRepo has Permissions: %-+v",
   447  					ctx.Doer,
   448  					ci.HeadRepo,
   449  					permHead)
   450  			}
   451  			ctx.NotFound("ParseCompareInfo", nil)
   452  			return nil
   453  		}
   454  		ctx.Data["CanWriteToHeadRepo"] = permHead.CanWrite(unit.TypeCode)
   455  	}
   456  
   457  	// If we have a rootRepo and it's different from:
   458  	// 1. the computed base
   459  	// 2. the computed head
   460  	// then get the branches of it
   461  	if rootRepo != nil &&
   462  		rootRepo.ID != ci.HeadRepo.ID &&
   463  		rootRepo.ID != baseRepo.ID {
   464  		canRead := access_model.CheckRepoUnitUser(ctx, rootRepo, ctx.Doer, unit.TypeCode)
   465  		if canRead {
   466  			ctx.Data["RootRepo"] = rootRepo
   467  			if !fileOnly {
   468  				branches, tags, err := getBranchesAndTagsForRepo(ctx, rootRepo)
   469  				if err != nil {
   470  					ctx.ServerError("GetBranchesForRepo", err)
   471  					return nil
   472  				}
   473  
   474  				ctx.Data["RootRepoBranches"] = branches
   475  				ctx.Data["RootRepoTags"] = tags
   476  			}
   477  		}
   478  	}
   479  
   480  	// If we have a ownForkRepo and it's different from:
   481  	// 1. The computed base
   482  	// 2. The computed head
   483  	// 3. The rootRepo (if we have one)
   484  	// then get the branches from it.
   485  	if ownForkRepo != nil &&
   486  		ownForkRepo.ID != ci.HeadRepo.ID &&
   487  		ownForkRepo.ID != baseRepo.ID &&
   488  		(rootRepo == nil || ownForkRepo.ID != rootRepo.ID) {
   489  		canRead := access_model.CheckRepoUnitUser(ctx, ownForkRepo, ctx.Doer, unit.TypeCode)
   490  		if canRead {
   491  			ctx.Data["OwnForkRepo"] = ownForkRepo
   492  			if !fileOnly {
   493  				branches, tags, err := getBranchesAndTagsForRepo(ctx, ownForkRepo)
   494  				if err != nil {
   495  					ctx.ServerError("GetBranchesForRepo", err)
   496  					return nil
   497  				}
   498  				ctx.Data["OwnForkRepoBranches"] = branches
   499  				ctx.Data["OwnForkRepoTags"] = tags
   500  			}
   501  		}
   502  	}
   503  
   504  	// Check if head branch is valid.
   505  	headIsCommit := ci.HeadGitRepo.IsCommitExist(ci.HeadBranch)
   506  	headIsBranch := ci.HeadGitRepo.IsBranchExist(ci.HeadBranch)
   507  	headIsTag := ci.HeadGitRepo.IsTagExist(ci.HeadBranch)
   508  	if !headIsCommit && !headIsBranch && !headIsTag {
   509  		// Check if headBranch is short sha commit hash
   510  		if headCommit, _ := ci.HeadGitRepo.GetCommit(ci.HeadBranch); headCommit != nil {
   511  			ci.HeadBranch = headCommit.ID.String()
   512  			ctx.Data["HeadBranch"] = ci.HeadBranch
   513  			headIsCommit = true
   514  		} else {
   515  			ctx.NotFound("IsRefExist", nil)
   516  			return nil
   517  		}
   518  	}
   519  	ctx.Data["HeadIsCommit"] = headIsCommit
   520  	ctx.Data["HeadIsBranch"] = headIsBranch
   521  	ctx.Data["HeadIsTag"] = headIsTag
   522  
   523  	// Treat as pull request if both references are branches
   524  	if ctx.Data["PageIsComparePull"] == nil {
   525  		ctx.Data["PageIsComparePull"] = headIsBranch && baseIsBranch
   526  	}
   527  
   528  	if ctx.Data["PageIsComparePull"] == true && !permBase.CanReadIssuesOrPulls(true) {
   529  		if log.IsTrace() {
   530  			log.Trace("Permission Denied: User: %-v cannot create/read pull requests in Repo: %-v\nUser in baseRepo has Permissions: %-+v",
   531  				ctx.Doer,
   532  				baseRepo,
   533  				permBase)
   534  		}
   535  		ctx.NotFound("ParseCompareInfo", nil)
   536  		return nil
   537  	}
   538  
   539  	baseBranchRef := ci.BaseBranch
   540  	if baseIsBranch {
   541  		baseBranchRef = git.BranchPrefix + ci.BaseBranch
   542  	} else if baseIsTag {
   543  		baseBranchRef = git.TagPrefix + ci.BaseBranch
   544  	}
   545  	headBranchRef := ci.HeadBranch
   546  	if headIsBranch {
   547  		headBranchRef = git.BranchPrefix + ci.HeadBranch
   548  	} else if headIsTag {
   549  		headBranchRef = git.TagPrefix + ci.HeadBranch
   550  	}
   551  
   552  	ci.CompareInfo, err = ci.HeadGitRepo.GetCompareInfo(baseRepo.RepoPath(), baseBranchRef, headBranchRef, ci.DirectComparison, fileOnly)
   553  	if err != nil {
   554  		ctx.ServerError("GetCompareInfo", err)
   555  		return nil
   556  	}
   557  	if ci.DirectComparison {
   558  		ctx.Data["BeforeCommitID"] = ci.CompareInfo.BaseCommitID
   559  	} else {
   560  		ctx.Data["BeforeCommitID"] = ci.CompareInfo.MergeBase
   561  	}
   562  
   563  	return ci
   564  }
   565  
   566  // PrepareCompareDiff renders compare diff page
   567  func PrepareCompareDiff(
   568  	ctx *context.Context,
   569  	ci *common.CompareInfo,
   570  	whitespaceBehavior git.TrustedCmdArgs,
   571  ) bool {
   572  	var (
   573  		repo  = ctx.Repo.Repository
   574  		err   error
   575  		title string
   576  	)
   577  
   578  	// Get diff information.
   579  	ctx.Data["CommitRepoLink"] = ci.HeadRepo.Link()
   580  
   581  	headCommitID := ci.CompareInfo.HeadCommitID
   582  
   583  	ctx.Data["AfterCommitID"] = headCommitID
   584  
   585  	if (headCommitID == ci.CompareInfo.MergeBase && !ci.DirectComparison) ||
   586  		headCommitID == ci.CompareInfo.BaseCommitID {
   587  		ctx.Data["IsNothingToCompare"] = true
   588  		if unit, err := repo.GetUnit(ctx, unit.TypePullRequests); err == nil {
   589  			config := unit.PullRequestsConfig()
   590  
   591  			if !config.AutodetectManualMerge {
   592  				allowEmptyPr := !(ci.BaseBranch == ci.HeadBranch && ctx.Repo.Repository.Name == ci.HeadRepo.Name)
   593  				ctx.Data["AllowEmptyPr"] = allowEmptyPr
   594  
   595  				return !allowEmptyPr
   596  			}
   597  
   598  			ctx.Data["AllowEmptyPr"] = false
   599  		}
   600  		return true
   601  	}
   602  
   603  	beforeCommitID := ci.CompareInfo.MergeBase
   604  	if ci.DirectComparison {
   605  		beforeCommitID = ci.CompareInfo.BaseCommitID
   606  	}
   607  
   608  	maxLines, maxFiles := setting.Git.MaxGitDiffLines, setting.Git.MaxGitDiffFiles
   609  	files := ctx.FormStrings("files")
   610  	if len(files) == 2 || len(files) == 1 {
   611  		maxLines, maxFiles = -1, -1
   612  	}
   613  
   614  	diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo,
   615  		&gitdiff.DiffOptions{
   616  			BeforeCommitID:     beforeCommitID,
   617  			AfterCommitID:      headCommitID,
   618  			SkipTo:             ctx.FormString("skip-to"),
   619  			MaxLines:           maxLines,
   620  			MaxLineCharacters:  setting.Git.MaxGitDiffLineCharacters,
   621  			MaxFiles:           maxFiles,
   622  			WhitespaceBehavior: whitespaceBehavior,
   623  			DirectComparison:   ci.DirectComparison,
   624  		}, ctx.FormStrings("files")...)
   625  	if err != nil {
   626  		ctx.ServerError("GetDiffRangeWithWhitespaceBehavior", err)
   627  		return false
   628  	}
   629  	ctx.Data["Diff"] = diff
   630  	ctx.Data["DiffNotAvailable"] = diff.NumFiles == 0
   631  
   632  	headCommit, err := ci.HeadGitRepo.GetCommit(headCommitID)
   633  	if err != nil {
   634  		ctx.ServerError("GetCommit", err)
   635  		return false
   636  	}
   637  
   638  	baseGitRepo := ctx.Repo.GitRepo
   639  
   640  	beforeCommit, err := baseGitRepo.GetCommit(beforeCommitID)
   641  	if err != nil {
   642  		ctx.ServerError("GetCommit", err)
   643  		return false
   644  	}
   645  
   646  	commits := git_model.ConvertFromGitCommit(ctx, ci.CompareInfo.Commits, ci.HeadRepo)
   647  	ctx.Data["Commits"] = commits
   648  	ctx.Data["CommitCount"] = len(commits)
   649  
   650  	if len(commits) == 1 {
   651  		c := commits[0]
   652  		title = strings.TrimSpace(c.UserCommit.Summary())
   653  
   654  		body := strings.Split(strings.TrimSpace(c.UserCommit.Message()), "\n")
   655  		if len(body) > 1 {
   656  			ctx.Data["content"] = strings.Join(body[1:], "\n")
   657  		}
   658  	} else {
   659  		title = ci.HeadBranch
   660  	}
   661  	if len(title) > 255 {
   662  		var trailer string
   663  		title, trailer = util.SplitStringAtByteN(title, 255)
   664  		if len(trailer) > 0 {
   665  			if ctx.Data["content"] != nil {
   666  				ctx.Data["content"] = fmt.Sprintf("%s\n\n%s", trailer, ctx.Data["content"])
   667  			} else {
   668  				ctx.Data["content"] = trailer + "\n"
   669  			}
   670  		}
   671  	}
   672  
   673  	ctx.Data["title"] = title
   674  	ctx.Data["Username"] = ci.HeadUser.Name
   675  	ctx.Data["Reponame"] = ci.HeadRepo.Name
   676  
   677  	setCompareContext(ctx, beforeCommit, headCommit, ci.HeadUser.Name, repo.Name)
   678  
   679  	return false
   680  }
   681  
   682  func getBranchesAndTagsForRepo(ctx gocontext.Context, repo *repo_model.Repository) (branches, tags []string, err error) {
   683  	gitRepo, err := gitrepo.OpenRepository(ctx, repo)
   684  	if err != nil {
   685  		return nil, nil, err
   686  	}
   687  	defer gitRepo.Close()
   688  
   689  	branches, err = git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
   690  		RepoID:          repo.ID,
   691  		ListOptions:     db.ListOptionsAll,
   692  		IsDeletedBranch: optional.Some(false),
   693  	})
   694  	if err != nil {
   695  		return nil, nil, err
   696  	}
   697  	tags, err = gitRepo.GetTags(0, 0)
   698  	if err != nil {
   699  		return nil, nil, err
   700  	}
   701  	return branches, tags, nil
   702  }
   703  
   704  // CompareDiff show different from one commit to another commit
   705  func CompareDiff(ctx *context.Context) {
   706  	ci := ParseCompareInfo(ctx)
   707  	defer func() {
   708  		if ci != nil && ci.HeadGitRepo != nil {
   709  			ci.HeadGitRepo.Close()
   710  		}
   711  	}()
   712  	if ctx.Written() {
   713  		return
   714  	}
   715  
   716  	ctx.Data["PullRequestWorkInProgressPrefixes"] = setting.Repository.PullRequest.WorkInProgressPrefixes
   717  	ctx.Data["DirectComparison"] = ci.DirectComparison
   718  	ctx.Data["OtherCompareSeparator"] = ".."
   719  	ctx.Data["CompareSeparator"] = "..."
   720  	if ci.DirectComparison {
   721  		ctx.Data["CompareSeparator"] = ".."
   722  		ctx.Data["OtherCompareSeparator"] = "..."
   723  	}
   724  
   725  	nothingToCompare := PrepareCompareDiff(ctx, ci,
   726  		gitdiff.GetWhitespaceFlag(ctx.Data["WhitespaceBehavior"].(string)))
   727  	if ctx.Written() {
   728  		return
   729  	}
   730  
   731  	baseTags, err := repo_model.GetTagNamesByRepoID(ctx, ctx.Repo.Repository.ID)
   732  	if err != nil {
   733  		ctx.ServerError("GetTagNamesByRepoID", err)
   734  		return
   735  	}
   736  	ctx.Data["Tags"] = baseTags
   737  
   738  	fileOnly := ctx.FormBool("file-only")
   739  	if fileOnly {
   740  		ctx.HTML(http.StatusOK, tplDiffBox)
   741  		return
   742  	}
   743  
   744  	headBranches, err := git_model.FindBranchNames(ctx, git_model.FindBranchOptions{
   745  		RepoID:          ci.HeadRepo.ID,
   746  		ListOptions:     db.ListOptionsAll,
   747  		IsDeletedBranch: optional.Some(false),
   748  	})
   749  	if err != nil {
   750  		ctx.ServerError("GetBranches", err)
   751  		return
   752  	}
   753  	ctx.Data["HeadBranches"] = headBranches
   754  
   755  	// For compare repo branches
   756  	PrepareBranchList(ctx)
   757  	if ctx.Written() {
   758  		return
   759  	}
   760  
   761  	headTags, err := repo_model.GetTagNamesByRepoID(ctx, ci.HeadRepo.ID)
   762  	if err != nil {
   763  		ctx.ServerError("GetTagNamesByRepoID", err)
   764  		return
   765  	}
   766  	ctx.Data["HeadTags"] = headTags
   767  
   768  	if ctx.Data["PageIsComparePull"] == true {
   769  		pr, err := issues_model.GetUnmergedPullRequest(ctx, ci.HeadRepo.ID, ctx.Repo.Repository.ID, ci.HeadBranch, ci.BaseBranch, issues_model.PullRequestFlowGithub)
   770  		if err != nil {
   771  			if !issues_model.IsErrPullRequestNotExist(err) {
   772  				ctx.ServerError("GetUnmergedPullRequest", err)
   773  				return
   774  			}
   775  		} else {
   776  			ctx.Data["HasPullRequest"] = true
   777  			if err := pr.LoadIssue(ctx); err != nil {
   778  				ctx.ServerError("LoadIssue", err)
   779  				return
   780  			}
   781  			ctx.Data["PullRequest"] = pr
   782  			ctx.HTML(http.StatusOK, tplCompareDiff)
   783  			return
   784  		}
   785  
   786  		if !nothingToCompare {
   787  			// Setup information for new form.
   788  			RetrieveRepoMetas(ctx, ctx.Repo.Repository, true)
   789  			if ctx.Written() {
   790  				return
   791  			}
   792  		}
   793  	}
   794  	beforeCommitID := ctx.Data["BeforeCommitID"].(string)
   795  	afterCommitID := ctx.Data["AfterCommitID"].(string)
   796  
   797  	separator := "..."
   798  	if ci.DirectComparison {
   799  		separator = ".."
   800  	}
   801  	ctx.Data["Title"] = "Comparing " + base.ShortSha(beforeCommitID) + separator + base.ShortSha(afterCommitID)
   802  
   803  	ctx.Data["IsDiffCompare"] = true
   804  	_, templateErrs := setTemplateIfExists(ctx, pullRequestTemplateKey, pullRequestTemplateCandidates)
   805  
   806  	if len(templateErrs) > 0 {
   807  		ctx.Flash.Warning(renderErrorOfTemplates(ctx, templateErrs), true)
   808  	}
   809  
   810  	if content, ok := ctx.Data["content"].(string); ok && content != "" {
   811  		// If a template content is set, prepend the "content". In this case that's only
   812  		// applicable if you have one commit to compare and that commit has a message.
   813  		// In that case the commit message will be prepend to the template body.
   814  		if templateContent, ok := ctx.Data[pullRequestTemplateKey].(string); ok && templateContent != "" {
   815  			// Re-use the same key as that's prioritized over the "content" key.
   816  			// Add two new lines between the content to ensure there's always at least
   817  			// one empty line between them.
   818  			ctx.Data[pullRequestTemplateKey] = content + "\n\n" + templateContent
   819  		}
   820  
   821  		// When using form fields, also add content to field with id "body".
   822  		if fields, ok := ctx.Data["Fields"].([]*api.IssueFormField); ok {
   823  			for _, field := range fields {
   824  				if field.ID == "body" {
   825  					if fieldValue, ok := field.Attributes["value"].(string); ok && fieldValue != "" {
   826  						field.Attributes["value"] = content + "\n\n" + fieldValue
   827  					} else {
   828  						field.Attributes["value"] = content
   829  					}
   830  				}
   831  			}
   832  		}
   833  	}
   834  
   835  	ctx.Data["IsProjectsEnabled"] = ctx.Repo.CanWrite(unit.TypeProjects)
   836  	ctx.Data["IsAttachmentEnabled"] = setting.Attachment.Enabled
   837  	upload.AddUploadContext(ctx, "comment")
   838  
   839  	ctx.Data["HasIssuesOrPullsWritePermission"] = ctx.Repo.CanWrite(unit.TypePullRequests)
   840  
   841  	if unit, err := ctx.Repo.Repository.GetUnit(ctx, unit.TypePullRequests); err == nil {
   842  		config := unit.PullRequestsConfig()
   843  		ctx.Data["AllowMaintainerEdit"] = config.DefaultAllowMaintainerEdit
   844  	} else {
   845  		ctx.Data["AllowMaintainerEdit"] = false
   846  	}
   847  
   848  	ctx.HTML(http.StatusOK, tplCompare)
   849  }
   850  
   851  // ExcerptBlob render blob excerpt contents
   852  func ExcerptBlob(ctx *context.Context) {
   853  	commitID := ctx.Params("sha")
   854  	lastLeft := ctx.FormInt("last_left")
   855  	lastRight := ctx.FormInt("last_right")
   856  	idxLeft := ctx.FormInt("left")
   857  	idxRight := ctx.FormInt("right")
   858  	leftHunkSize := ctx.FormInt("left_hunk_size")
   859  	rightHunkSize := ctx.FormInt("right_hunk_size")
   860  	anchor := ctx.FormString("anchor")
   861  	direction := ctx.FormString("direction")
   862  	filePath := ctx.FormString("path")
   863  	gitRepo := ctx.Repo.GitRepo
   864  	if ctx.FormBool("wiki") {
   865  		var err error
   866  		gitRepo, err = gitrepo.OpenWikiRepository(ctx, ctx.Repo.Repository)
   867  		if err != nil {
   868  			ctx.ServerError("OpenRepository", err)
   869  			return
   870  		}
   871  		defer gitRepo.Close()
   872  	}
   873  	chunkSize := gitdiff.BlobExcerptChunkSize
   874  	commit, err := gitRepo.GetCommit(commitID)
   875  	if err != nil {
   876  		ctx.Error(http.StatusInternalServerError, "GetCommit")
   877  		return
   878  	}
   879  	section := &gitdiff.DiffSection{
   880  		FileName: filePath,
   881  		Name:     filePath,
   882  	}
   883  	if direction == "up" && (idxLeft-lastLeft) > chunkSize {
   884  		idxLeft -= chunkSize
   885  		idxRight -= chunkSize
   886  		leftHunkSize += chunkSize
   887  		rightHunkSize += chunkSize
   888  		section.Lines, err = getExcerptLines(commit, filePath, idxLeft-1, idxRight-1, chunkSize)
   889  	} else if direction == "down" && (idxLeft-lastLeft) > chunkSize {
   890  		section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, chunkSize)
   891  		lastLeft += chunkSize
   892  		lastRight += chunkSize
   893  	} else {
   894  		offset := -1
   895  		if direction == "down" {
   896  			offset = 0
   897  		}
   898  		section.Lines, err = getExcerptLines(commit, filePath, lastLeft, lastRight, idxRight-lastRight+offset)
   899  		leftHunkSize = 0
   900  		rightHunkSize = 0
   901  		idxLeft = lastLeft
   902  		idxRight = lastRight
   903  	}
   904  	if err != nil {
   905  		ctx.Error(http.StatusInternalServerError, "getExcerptLines")
   906  		return
   907  	}
   908  	if idxRight > lastRight {
   909  		lineText := " "
   910  		if rightHunkSize > 0 || leftHunkSize > 0 {
   911  			lineText = fmt.Sprintf("@@ -%d,%d +%d,%d @@\n", idxLeft, leftHunkSize, idxRight, rightHunkSize)
   912  		}
   913  		lineText = html.EscapeString(lineText)
   914  		lineSection := &gitdiff.DiffLine{
   915  			Type:    gitdiff.DiffLineSection,
   916  			Content: lineText,
   917  			SectionInfo: &gitdiff.DiffLineSectionInfo{
   918  				Path:          filePath,
   919  				LastLeftIdx:   lastLeft,
   920  				LastRightIdx:  lastRight,
   921  				LeftIdx:       idxLeft,
   922  				RightIdx:      idxRight,
   923  				LeftHunkSize:  leftHunkSize,
   924  				RightHunkSize: rightHunkSize,
   925  			},
   926  		}
   927  		if direction == "up" {
   928  			section.Lines = append([]*gitdiff.DiffLine{lineSection}, section.Lines...)
   929  		} else if direction == "down" {
   930  			section.Lines = append(section.Lines, lineSection)
   931  		}
   932  	}
   933  	ctx.Data["section"] = section
   934  	ctx.Data["FileNameHash"] = git.HashFilePathForWebUI(filePath)
   935  	ctx.Data["AfterCommitID"] = commitID
   936  	ctx.Data["Anchor"] = anchor
   937  	ctx.HTML(http.StatusOK, tplBlobExcerpt)
   938  }
   939  
   940  func getExcerptLines(commit *git.Commit, filePath string, idxLeft, idxRight, chunkSize int) ([]*gitdiff.DiffLine, error) {
   941  	blob, err := commit.Tree.GetBlobByPath(filePath)
   942  	if err != nil {
   943  		return nil, err
   944  	}
   945  	reader, err := blob.DataAsync()
   946  	if err != nil {
   947  		return nil, err
   948  	}
   949  	defer reader.Close()
   950  	scanner := bufio.NewScanner(reader)
   951  	var diffLines []*gitdiff.DiffLine
   952  	for line := 0; line < idxRight+chunkSize; line++ {
   953  		if ok := scanner.Scan(); !ok {
   954  			break
   955  		}
   956  		if line < idxRight {
   957  			continue
   958  		}
   959  		lineText := scanner.Text()
   960  		diffLine := &gitdiff.DiffLine{
   961  			LeftIdx:  idxLeft + (line - idxRight) + 1,
   962  			RightIdx: line + 1,
   963  			Type:     gitdiff.DiffLinePlain,
   964  			Content:  " " + lineText,
   965  		}
   966  		diffLines = append(diffLines, diffLine)
   967  	}
   968  	if err = scanner.Err(); err != nil {
   969  		return nil, fmt.Errorf("getExcerptLines scan: %w", err)
   970  	}
   971  	return diffLines, nil
   972  }