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