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 }