loading up the forgejo repo on tangled to test page performance
at forgejo 43 kB view raw
1// Copyright 2014 The Gogs Authors. All rights reserved. 2// Copyright 2019 The Gitea Authors. All rights reserved. 3// SPDX-License-Identifier: MIT 4 5package gitdiff 6 7import ( 8 "bufio" 9 "bytes" 10 "cmp" 11 "context" 12 "fmt" 13 "html" 14 "html/template" 15 "io" 16 "net/url" 17 "strings" 18 "time" 19 20 "forgejo.org/models/db" 21 git_model "forgejo.org/models/git" 22 issues_model "forgejo.org/models/issues" 23 pull_model "forgejo.org/models/pull" 24 user_model "forgejo.org/models/user" 25 "forgejo.org/modules/analyze" 26 "forgejo.org/modules/charset" 27 "forgejo.org/modules/git" 28 "forgejo.org/modules/highlight" 29 "forgejo.org/modules/lfs" 30 "forgejo.org/modules/log" 31 "forgejo.org/modules/setting" 32 "forgejo.org/modules/translation" 33 34 "github.com/sergi/go-diff/diffmatchpatch" 35 stdcharset "golang.org/x/net/html/charset" 36 "golang.org/x/text/encoding" 37 "golang.org/x/text/transform" 38) 39 40// DiffLineType represents the type of DiffLine. 41type DiffLineType uint8 42 43// DiffLineType possible values. 44const ( 45 DiffLinePlain DiffLineType = iota + 1 46 DiffLineAdd 47 DiffLineDel 48 DiffLineSection 49) 50 51// DiffFileType represents the type of DiffFile. 52type DiffFileType uint8 53 54// DiffFileType possible values. 55const ( 56 DiffFileAdd DiffFileType = iota + 1 57 DiffFileChange 58 DiffFileDel 59 DiffFileRename 60 DiffFileCopy 61) 62 63// DiffLineExpandDirection represents the DiffLineSection expand direction 64type DiffLineExpandDirection uint8 65 66// DiffLineExpandDirection possible values. 67const ( 68 DiffLineExpandNone DiffLineExpandDirection = iota + 1 69 DiffLineExpandSingle 70 DiffLineExpandUpDown 71 DiffLineExpandUp 72 DiffLineExpandDown 73) 74 75// DiffLine represents a line difference in a DiffSection. 76type DiffLine struct { 77 LeftIdx int 78 RightIdx int 79 Match int 80 Type DiffLineType 81 Content string 82 Conversations []issues_model.CodeConversation 83 SectionInfo *DiffLineSectionInfo 84} 85 86// DiffLineSectionInfo represents diff line section meta data 87type DiffLineSectionInfo struct { 88 Path string 89 LastLeftIdx int 90 LastRightIdx int 91 LeftIdx int 92 RightIdx int 93 LeftHunkSize int 94 RightHunkSize int 95} 96 97// BlobExcerptChunkSize represent max lines of excerpt 98const BlobExcerptChunkSize = 20 99 100// GetType returns the type of DiffLine. 101func (d *DiffLine) GetType() int { 102 return int(d.Type) 103} 104 105// GetHTMLDiffLineType returns the diff line type name for HTML 106func (d *DiffLine) GetHTMLDiffLineType() string { 107 switch d.Type { 108 case DiffLineAdd: 109 return "add" 110 case DiffLineDel: 111 return "del" 112 case DiffLineSection: 113 return "tag" 114 } 115 return "same" 116} 117 118// CanComment returns whether a line can get commented 119func (d *DiffLine) CanComment() bool { 120 return len(d.Conversations) == 0 && d.Type != DiffLineSection 121} 122 123// GetCommentSide returns the comment side of the first comment, if not set returns empty string 124func (d *DiffLine) GetCommentSide() string { 125 if len(d.Conversations) == 0 || len(d.Conversations[0]) == 0 { 126 return "" 127 } 128 return d.Conversations[0][0].DiffSide() 129} 130 131// GetLineTypeMarker returns the line type marker 132func (d *DiffLine) GetLineTypeMarker() string { 133 if strings.IndexByte(" +-", d.Content[0]) > -1 { 134 return d.Content[0:1] 135 } 136 return "" 137} 138 139// GetBlobExcerptQuery builds query string to get blob excerpt 140func (d *DiffLine) GetBlobExcerptQuery() string { 141 query := fmt.Sprintf( 142 "last_left=%d&last_right=%d&"+ 143 "left=%d&right=%d&"+ 144 "left_hunk_size=%d&right_hunk_size=%d&"+ 145 "path=%s", 146 d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx, 147 d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx, 148 d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize, 149 url.QueryEscape(d.SectionInfo.Path)) 150 return query 151} 152 153// GetExpandDirection gets DiffLineExpandDirection 154func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { 155 if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { 156 return DiffLineExpandNone 157 } 158 if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { 159 return DiffLineExpandUp 160 } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 { 161 return DiffLineExpandUpDown 162 } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { 163 return DiffLineExpandDown 164 } 165 return DiffLineExpandSingle 166} 167 168func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { 169 leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line) 170 171 return &DiffLineSectionInfo{ 172 Path: treePath, 173 LastLeftIdx: lastLeftIdx, 174 LastRightIdx: lastRightIdx, 175 LeftIdx: leftLine, 176 RightIdx: rightLine, 177 LeftHunkSize: leftHunk, 178 RightHunkSize: righHunk, 179 } 180} 181 182// escape a line's content or return <br> needed for copy/paste purposes 183func getLineContent(content string, locale translation.Locale) DiffInline { 184 if len(content) > 0 { 185 return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)), locale) 186 } 187 return DiffInline{EscapeStatus: &charset.EscapeStatus{}, Content: "<br>"} 188} 189 190// DiffSection represents a section of a DiffFile. 191type DiffSection struct { 192 file *DiffFile 193 FileName string 194 Name string 195 Lines []*DiffLine 196} 197 198var ( 199 addedCodePrefix = []byte(`<span class="added-code">`) 200 removedCodePrefix = []byte(`<span class="removed-code">`) 201 codeTagSuffix = []byte(`</span>`) 202) 203 204func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string { 205 buf := bytes.NewBuffer(nil) 206 // restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary 207 for _, tag := range lineWrapperTags { 208 buf.WriteString(tag) 209 } 210 for _, diff := range diffs { 211 switch { 212 case diff.Type == diffmatchpatch.DiffEqual: 213 buf.WriteString(diff.Text) 214 case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: 215 buf.Write(addedCodePrefix) 216 buf.WriteString(diff.Text) 217 buf.Write(codeTagSuffix) 218 case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: 219 buf.Write(removedCodePrefix) 220 buf.WriteString(diff.Text) 221 buf.Write(codeTagSuffix) 222 } 223 } 224 for range lineWrapperTags { 225 buf.WriteString("</span>") 226 } 227 return buf.String() 228} 229 230// GetLine gets a specific line by type (add or del) and file line number 231func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine { 232 var ( 233 difference = 0 234 addCount = 0 235 delCount = 0 236 matchDiffLine *DiffLine 237 ) 238 239LOOP: 240 for _, diffLine := range diffSection.Lines { 241 switch diffLine.Type { 242 case DiffLineAdd: 243 addCount++ 244 case DiffLineDel: 245 delCount++ 246 default: 247 if matchDiffLine != nil { 248 break LOOP 249 } 250 difference = diffLine.RightIdx - diffLine.LeftIdx 251 addCount = 0 252 delCount = 0 253 } 254 255 switch lineType { 256 case DiffLineDel: 257 if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference { 258 matchDiffLine = diffLine 259 } 260 case DiffLineAdd: 261 if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference { 262 matchDiffLine = diffLine 263 } 264 } 265 } 266 267 if addCount == delCount { 268 return matchDiffLine 269 } 270 return nil 271} 272 273var diffMatchPatch = diffmatchpatch.New() 274 275func init() { 276 diffMatchPatch.DiffEditCost = 100 277} 278 279// DiffInline is a struct that has a content and escape status 280type DiffInline struct { 281 EscapeStatus *charset.EscapeStatus 282 Content template.HTML 283} 284 285// DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped 286func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline { 287 status, content := charset.EscapeControlHTML(s, locale, charset.DiffContext) 288 return DiffInline{EscapeStatus: status, Content: content} 289} 290 291// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped 292func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline { 293 highlighted, _ := highlight.Code(fileName, language, code) 294 status, content := charset.EscapeControlHTML(highlighted, locale, charset.DiffContext) 295 return DiffInline{EscapeStatus: status, Content: content} 296} 297 298// GetComputedInlineDiffFor computes inline diff for the given line. 299func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline { 300 if setting.Git.DisableDiffHighlight { 301 return getLineContent(diffLine.Content[1:], locale) 302 } 303 304 var ( 305 compareDiffLine *DiffLine 306 diff1 string 307 diff2 string 308 ) 309 310 language := "" 311 if diffSection.file != nil { 312 language = diffSection.file.Language 313 } 314 315 // try to find equivalent diff line. ignore, otherwise 316 switch diffLine.Type { 317 case DiffLineSection: 318 return getLineContent(diffLine.Content[1:], locale) 319 case DiffLineAdd: 320 compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx) 321 if compareDiffLine == nil { 322 return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale) 323 } 324 diff1 = compareDiffLine.Content 325 diff2 = diffLine.Content 326 case DiffLineDel: 327 compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx) 328 if compareDiffLine == nil { 329 return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale) 330 } 331 diff1 = diffLine.Content 332 diff2 = compareDiffLine.Content 333 default: 334 if strings.IndexByte(" +-", diffLine.Content[0]) > -1 { 335 return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale) 336 } 337 return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content, locale) 338 } 339 340 hcd := NewHighlightCodeDiff() 341 diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:]) 342 // it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back 343 // if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)" 344 diffHTML := diffToHTML(nil, diffRecord, diffLine.Type) 345 return DiffInlineWithUnicodeEscape(template.HTML(diffHTML), locale) 346} 347 348// DiffFile represents a file diff. 349type DiffFile struct { 350 Name string 351 NameHash string 352 OldName string 353 Index int 354 Addition, Deletion int 355 Type DiffFileType 356 IsCreated bool 357 IsDeleted bool 358 IsBin bool 359 IsLFSFile bool 360 IsRenamed bool 361 IsAmbiguous bool 362 IsSubmodule bool 363 Sections []*DiffSection 364 IsIncomplete bool 365 IsIncompleteLineTooLong bool 366 IsProtected bool 367 IsGenerated bool 368 IsVendored bool 369 IsViewed bool // User specific 370 HasChangedSinceLastReview bool // User specific 371 Language string 372 Mode string 373 OldMode string 374} 375 376// GetType returns type of diff file. 377func (diffFile *DiffFile) GetType() int { 378 return int(diffFile.Type) 379} 380 381// GetTailSection creates a fake DiffLineSection if the last section is not the end of the file 382func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommit, rightCommit *git.Commit) *DiffSection { 383 if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile { 384 return nil 385 } 386 387 lastSection := diffFile.Sections[len(diffFile.Sections)-1] 388 lastLine := lastSection.Lines[len(lastSection.Lines)-1] 389 leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name) 390 rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name) 391 if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx { 392 return nil 393 } 394 tailDiffLine := &DiffLine{ 395 Type: DiffLineSection, 396 Content: " ", 397 SectionInfo: &DiffLineSectionInfo{ 398 Path: diffFile.Name, 399 LastLeftIdx: lastLine.LeftIdx, 400 LastRightIdx: lastLine.RightIdx, 401 LeftIdx: leftLineCount, 402 RightIdx: rightLineCount, 403 }, 404 } 405 tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}} 406 return tailSection 407} 408 409// GetDiffFileName returns the name of the diff file, or its old name in case it was deleted 410func (diffFile *DiffFile) GetDiffFileName() string { 411 if diffFile.Name == "" { 412 return diffFile.OldName 413 } 414 return diffFile.Name 415} 416 417func (diffFile *DiffFile) ShouldBeHidden() bool { 418 return diffFile.IsGenerated || diffFile.IsViewed 419} 420 421func (diffFile *DiffFile) ModeTranslationKey(mode string) string { 422 switch mode { 423 case "040000": 424 return "git.filemode.directory" 425 case "100644": 426 return "git.filemode.normal_file" 427 case "100755": 428 return "git.filemode.executable_file" 429 case "120000": 430 return "git.filemode.symbolic_link" 431 case "160000": 432 return "git.filemode.submodule" 433 default: 434 return mode 435 } 436} 437 438func getCommitFileLineCount(commit *git.Commit, filePath string) int { 439 blob, err := commit.GetBlobByPath(filePath) 440 if err != nil { 441 return 0 442 } 443 lineCount, err := blob.GetBlobLineCount() 444 if err != nil { 445 return 0 446 } 447 return lineCount 448} 449 450// Diff represents a difference between two git trees. 451type Diff struct { 452 Start, End string 453 NumFiles int 454 TotalAddition, TotalDeletion int 455 Files []*DiffFile 456 IsIncomplete bool 457 NumViewedFiles int // user-specific 458} 459 460// LoadComments loads comments into each line 461func (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error { 462 allConversations, err := issues_model.FetchCodeConversations(ctx, issue, currentUser, showOutdatedComments) 463 if err != nil { 464 return err 465 } 466 for _, file := range diff.Files { 467 if lineCommits, ok := allConversations[file.Name]; ok { 468 for _, section := range file.Sections { 469 for _, line := range section.Lines { 470 if conversations, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { 471 line.Conversations = append(line.Conversations, conversations...) 472 } 473 if comments, ok := lineCommits[int64(line.RightIdx)]; ok { 474 line.Conversations = append(line.Conversations, comments...) 475 } 476 } 477 } 478 } 479 } 480 return nil 481} 482 483const cmdDiffHead = "diff --git " 484 485// ParsePatch builds a Diff object from a io.Reader and some parameters. 486func ParsePatch(ctx context.Context, maxLines, maxLineCharacters, maxFiles int, reader io.Reader, skipToFile string) (*Diff, error) { 487 log.Debug("ParsePatch(%d, %d, %d, ..., %s)", maxLines, maxLineCharacters, maxFiles, skipToFile) 488 var curFile *DiffFile 489 490 skipping := skipToFile != "" 491 492 diff := &Diff{Files: make([]*DiffFile, 0)} 493 494 sb := strings.Builder{} 495 496 // OK let's set a reasonable buffer size. 497 // This should be at least the size of maxLineCharacters or 4096 whichever is larger. 498 readerSize := maxLineCharacters 499 if readerSize < 4096 { 500 readerSize = 4096 501 } 502 503 input := bufio.NewReaderSize(reader, readerSize) 504 line, err := input.ReadString('\n') 505 if err != nil { 506 if err == io.EOF { 507 return diff, nil 508 } 509 return diff, err 510 } 511 512 prepareValue := func(s, p string) string { 513 return strings.TrimSpace(strings.TrimPrefix(s, p)) 514 } 515 516parsingLoop: 517 for { 518 // 1. A patch file always begins with `diff --git ` + `a/path b/path` (possibly quoted) 519 // if it does not we have bad input! 520 if !strings.HasPrefix(line, cmdDiffHead) { 521 return diff, fmt.Errorf("invalid first file line: %s", line) 522 } 523 524 if maxFiles > -1 && len(diff.Files) >= maxFiles { 525 lastFile := createDiffFile(diff, line) 526 diff.End = lastFile.Name 527 diff.IsIncomplete = true 528 break parsingLoop 529 } 530 531 curFile = createDiffFile(diff, line) 532 if skipping { 533 if curFile.Name != skipToFile { 534 line, err = skipToNextDiffHead(input) 535 if err != nil { 536 if err == io.EOF { 537 return diff, nil 538 } 539 return diff, err 540 } 541 continue 542 } 543 skipping = false 544 } 545 546 diff.Files = append(diff.Files, curFile) 547 548 // 2. It is followed by one or more extended header lines: 549 // 550 // old mode <mode> 551 // new mode <mode> 552 // deleted file mode <mode> 553 // new file mode <mode> 554 // copy from <path> 555 // copy to <path> 556 // rename from <path> 557 // rename to <path> 558 // similarity index <number> 559 // dissimilarity index <number> 560 // index <hash>..<hash> <mode> 561 // 562 // * <mode> 6-digit octal numbers including the file type and file permission bits. 563 // * <path> does not include the a/ and b/ prefixes 564 // * <number> percentage of unchanged lines for similarity, percentage of changed 565 // lines dissimilarity as integer rounded down with terminal %. 100% => equal files. 566 // * The index line includes the blob object names before and after the change. 567 // The <mode> is included if the file mode does not change; otherwise, separate 568 // lines indicate the old and the new mode. 569 // 3. Following this header the "standard unified" diff format header may be encountered: (but not for every case...) 570 // 571 // --- a/<path> 572 // +++ b/<path> 573 // 574 // With multiple hunks 575 // 576 // @@ <hunk descriptor> @@ 577 // +added line 578 // -removed line 579 // unchanged line 580 // 581 // 4. Binary files get: 582 // 583 // Binary files a/<path> and b/<path> differ 584 // 585 // but one of a/<path> and b/<path> could be /dev/null. 586 curFileLoop: 587 for { 588 line, err = input.ReadString('\n') 589 if err != nil { 590 if err != io.EOF { 591 return diff, err 592 } 593 break parsingLoop 594 } 595 596 switch { 597 case strings.HasPrefix(line, cmdDiffHead): 598 break curFileLoop 599 case strings.HasPrefix(line, "old mode ") || 600 strings.HasPrefix(line, "new mode "): 601 602 if strings.HasPrefix(line, "old mode ") { 603 curFile.OldMode = prepareValue(line, "old mode ") 604 } 605 if strings.HasPrefix(line, "new mode ") { 606 curFile.Mode = prepareValue(line, "new mode ") 607 } 608 609 if strings.HasSuffix(line, " 160000\n") { 610 curFile.IsSubmodule = true 611 } 612 case strings.HasPrefix(line, "rename from "): 613 curFile.IsRenamed = true 614 curFile.Type = DiffFileRename 615 if curFile.IsAmbiguous { 616 curFile.OldName = prepareValue(line, "rename from ") 617 } 618 case strings.HasPrefix(line, "rename to "): 619 curFile.IsRenamed = true 620 curFile.Type = DiffFileRename 621 if curFile.IsAmbiguous { 622 curFile.Name = prepareValue(line, "rename to ") 623 curFile.IsAmbiguous = false 624 } 625 case strings.HasPrefix(line, "copy from "): 626 curFile.IsRenamed = true 627 curFile.Type = DiffFileCopy 628 if curFile.IsAmbiguous { 629 curFile.OldName = prepareValue(line, "copy from ") 630 } 631 case strings.HasPrefix(line, "copy to "): 632 curFile.IsRenamed = true 633 curFile.Type = DiffFileCopy 634 if curFile.IsAmbiguous { 635 curFile.Name = prepareValue(line, "copy to ") 636 curFile.IsAmbiguous = false 637 } 638 case strings.HasPrefix(line, "new file"): 639 curFile.Type = DiffFileAdd 640 curFile.IsCreated = true 641 if strings.HasPrefix(line, "new file mode ") { 642 curFile.Mode = prepareValue(line, "new file mode ") 643 } 644 if strings.HasSuffix(line, " 160000\n") { 645 curFile.IsSubmodule = true 646 } 647 case strings.HasPrefix(line, "deleted"): 648 curFile.Type = DiffFileDel 649 curFile.IsDeleted = true 650 if strings.HasSuffix(line, " 160000\n") { 651 curFile.IsSubmodule = true 652 } 653 case strings.HasPrefix(line, "index"): 654 if strings.HasSuffix(line, " 160000\n") { 655 curFile.IsSubmodule = true 656 } 657 case strings.HasPrefix(line, "similarity index 100%"): 658 curFile.Type = DiffFileRename 659 case strings.HasPrefix(line, "Binary"): 660 curFile.IsBin = true 661 case strings.HasPrefix(line, "--- "): 662 // Handle ambiguous filenames 663 if curFile.IsAmbiguous { 664 // The shortest string that can end up here is: 665 // "--- a\t\n" without the quotes. 666 // This line has a len() of 7 but doesn't contain a oldName. 667 // So the amount that the line need is at least 8 or more. 668 // The code will otherwise panic for a out-of-bounds. 669 if len(line) > 7 && line[4] == 'a' { 670 curFile.OldName = line[6 : len(line)-1] 671 if line[len(line)-2] == '\t' { 672 curFile.OldName = curFile.OldName[:len(curFile.OldName)-1] 673 } 674 } else { 675 curFile.OldName = "" 676 } 677 } 678 // Otherwise do nothing with this line 679 case strings.HasPrefix(line, "+++ "): 680 // Handle ambiguous filenames 681 if curFile.IsAmbiguous { 682 if len(line) > 6 && line[4] == 'b' { 683 curFile.Name = line[6 : len(line)-1] 684 if line[len(line)-2] == '\t' { 685 curFile.Name = curFile.Name[:len(curFile.Name)-1] 686 } 687 if curFile.OldName == "" { 688 curFile.OldName = curFile.Name 689 } 690 } else { 691 curFile.Name = curFile.OldName 692 } 693 curFile.IsAmbiguous = false 694 } 695 // Otherwise do nothing with this line, but now switch to parsing hunks 696 lineBytes, isFragment, err := parseHunks(ctx, curFile, maxLines, maxLineCharacters, input) 697 diff.TotalAddition += curFile.Addition 698 diff.TotalDeletion += curFile.Deletion 699 if err != nil { 700 if err != io.EOF { 701 return diff, err 702 } 703 break parsingLoop 704 } 705 sb.Reset() 706 _, _ = sb.Write(lineBytes) 707 for isFragment { 708 lineBytes, isFragment, err = input.ReadLine() 709 if err != nil { 710 // Now by the definition of ReadLine this cannot be io.EOF 711 return diff, fmt.Errorf("unable to ReadLine: %w", err) 712 } 713 _, _ = sb.Write(lineBytes) 714 } 715 line = sb.String() 716 sb.Reset() 717 718 break curFileLoop 719 } 720 } 721 } 722 723 // TODO: There are numerous issues with this: 724 // - we might want to consider detecting encoding while parsing but... 725 // - we're likely to fail to get the correct encoding here anyway as we won't have enough information 726 diffLineTypeBuffers := make(map[DiffLineType]*bytes.Buffer, 3) 727 diffLineTypeDecoders := make(map[DiffLineType]*encoding.Decoder, 3) 728 diffLineTypeBuffers[DiffLinePlain] = new(bytes.Buffer) 729 diffLineTypeBuffers[DiffLineAdd] = new(bytes.Buffer) 730 diffLineTypeBuffers[DiffLineDel] = new(bytes.Buffer) 731 for _, f := range diff.Files { 732 f.NameHash = git.HashFilePathForWebUI(f.Name) 733 734 for _, buffer := range diffLineTypeBuffers { 735 buffer.Reset() 736 } 737 for _, sec := range f.Sections { 738 for _, l := range sec.Lines { 739 if l.Type == DiffLineSection { 740 continue 741 } 742 diffLineTypeBuffers[l.Type].WriteString(l.Content[1:]) 743 diffLineTypeBuffers[l.Type].WriteString("\n") 744 } 745 } 746 for lineType, buffer := range diffLineTypeBuffers { 747 diffLineTypeDecoders[lineType] = nil 748 if buffer.Len() == 0 { 749 continue 750 } 751 charsetLabel, err := charset.DetectEncoding(buffer.Bytes()) 752 if charsetLabel != "UTF-8" && err == nil { 753 encoding, _ := stdcharset.Lookup(charsetLabel) 754 if encoding != nil { 755 diffLineTypeDecoders[lineType] = encoding.NewDecoder() 756 } 757 } 758 } 759 for _, sec := range f.Sections { 760 for _, l := range sec.Lines { 761 decoder := diffLineTypeDecoders[l.Type] 762 if decoder != nil { 763 if c, _, err := transform.String(decoder, l.Content[1:]); err == nil { 764 l.Content = l.Content[0:1] + c 765 } 766 } 767 } 768 } 769 } 770 771 diff.NumFiles = len(diff.Files) 772 return diff, nil 773} 774 775func skipToNextDiffHead(input *bufio.Reader) (line string, err error) { 776 // need to skip until the next cmdDiffHead 777 var isFragment, wasFragment bool 778 var lineBytes []byte 779 for { 780 lineBytes, isFragment, err = input.ReadLine() 781 if err != nil { 782 return "", err 783 } 784 if wasFragment { 785 wasFragment = isFragment 786 continue 787 } 788 if bytes.HasPrefix(lineBytes, []byte(cmdDiffHead)) { 789 break 790 } 791 wasFragment = isFragment 792 } 793 line = string(lineBytes) 794 if isFragment { 795 var tail string 796 tail, err = input.ReadString('\n') 797 if err != nil { 798 return "", err 799 } 800 line += tail 801 } 802 return line, err 803} 804 805func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharacters int, input *bufio.Reader) (lineBytes []byte, isFragment bool, err error) { 806 sb := strings.Builder{} 807 808 var ( 809 curSection *DiffSection 810 curFileLinesCount int 811 curFileLFSPrefix bool 812 ) 813 814 lastLeftIdx := -1 815 leftLine, rightLine := 1, 1 816 817 for { 818 for isFragment { 819 curFile.IsIncomplete = true 820 curFile.IsIncompleteLineTooLong = true 821 _, isFragment, err = input.ReadLine() 822 if err != nil { 823 // Now by the definition of ReadLine this cannot be io.EOF 824 return nil, false, fmt.Errorf("unable to ReadLine: %w", err) 825 } 826 } 827 sb.Reset() 828 lineBytes, isFragment, err = input.ReadLine() 829 if err != nil { 830 if err == io.EOF { 831 return lineBytes, isFragment, err 832 } 833 err = fmt.Errorf("unable to ReadLine: %w", err) 834 return nil, false, err 835 } 836 if lineBytes[0] == 'd' { 837 // End of hunks 838 return lineBytes, isFragment, err 839 } 840 841 switch lineBytes[0] { 842 case '@': 843 if maxLines > -1 && curFileLinesCount >= maxLines { 844 curFile.IsIncomplete = true 845 continue 846 } 847 848 _, _ = sb.Write(lineBytes) 849 for isFragment { 850 // This is very odd indeed - we're in a section header and the line is too long 851 // This really shouldn't happen... 852 lineBytes, isFragment, err = input.ReadLine() 853 if err != nil { 854 // Now by the definition of ReadLine this cannot be io.EOF 855 return nil, false, fmt.Errorf("unable to ReadLine: %w", err) 856 } 857 _, _ = sb.Write(lineBytes) 858 } 859 line := sb.String() 860 861 // Create a new section to represent this hunk 862 curSection = &DiffSection{file: curFile} 863 lastLeftIdx = -1 864 curFile.Sections = append(curFile.Sections, curSection) 865 866 lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1) 867 diffLine := &DiffLine{ 868 Type: DiffLineSection, 869 Content: line, 870 SectionInfo: lineSectionInfo, 871 } 872 curSection.Lines = append(curSection.Lines, diffLine) 873 curSection.FileName = curFile.Name 874 // update line number. 875 leftLine = lineSectionInfo.LeftIdx 876 rightLine = lineSectionInfo.RightIdx 877 continue 878 case '\\': 879 if maxLines > -1 && curFileLinesCount >= maxLines { 880 curFile.IsIncomplete = true 881 continue 882 } 883 // This is used only to indicate that the current file does not have a terminal newline 884 if !bytes.Equal(lineBytes, []byte("\\ No newline at end of file")) { 885 return nil, false, fmt.Errorf("unexpected line in hunk: %s", string(lineBytes)) 886 } 887 // Technically this should be the end the file! 888 // FIXME: we should be putting a marker at the end of the file if there is no terminal new line 889 continue 890 case '+': 891 curFileLinesCount++ 892 curFile.Addition++ 893 if maxLines > -1 && curFileLinesCount >= maxLines { 894 curFile.IsIncomplete = true 895 continue 896 } 897 diffLine := &DiffLine{Type: DiffLineAdd, RightIdx: rightLine, Match: -1} 898 rightLine++ 899 if curSection == nil { 900 // Create a new section to represent this hunk 901 curSection = &DiffSection{file: curFile} 902 curFile.Sections = append(curFile.Sections, curSection) 903 lastLeftIdx = -1 904 } 905 if lastLeftIdx > -1 { 906 diffLine.Match = lastLeftIdx 907 curSection.Lines[lastLeftIdx].Match = len(curSection.Lines) 908 lastLeftIdx++ 909 if lastLeftIdx >= len(curSection.Lines) || curSection.Lines[lastLeftIdx].Type != DiffLineDel { 910 lastLeftIdx = -1 911 } 912 } 913 curSection.Lines = append(curSection.Lines, diffLine) 914 case '-': 915 curFileLinesCount++ 916 curFile.Deletion++ 917 if maxLines > -1 && curFileLinesCount >= maxLines { 918 curFile.IsIncomplete = true 919 continue 920 } 921 diffLine := &DiffLine{Type: DiffLineDel, LeftIdx: leftLine, Match: -1} 922 if leftLine > 0 { 923 leftLine++ 924 } 925 if curSection == nil { 926 // Create a new section to represent this hunk 927 curSection = &DiffSection{file: curFile} 928 curFile.Sections = append(curFile.Sections, curSection) 929 lastLeftIdx = -1 930 } 931 if len(curSection.Lines) == 0 || curSection.Lines[len(curSection.Lines)-1].Type != DiffLineDel { 932 lastLeftIdx = len(curSection.Lines) 933 } 934 curSection.Lines = append(curSection.Lines, diffLine) 935 case ' ': 936 curFileLinesCount++ 937 if maxLines > -1 && curFileLinesCount >= maxLines { 938 curFile.IsIncomplete = true 939 continue 940 } 941 diffLine := &DiffLine{Type: DiffLinePlain, LeftIdx: leftLine, RightIdx: rightLine} 942 leftLine++ 943 rightLine++ 944 lastLeftIdx = -1 945 if curSection == nil { 946 // Create a new section to represent this hunk 947 curSection = &DiffSection{file: curFile} 948 curFile.Sections = append(curFile.Sections, curSection) 949 } 950 curSection.Lines = append(curSection.Lines, diffLine) 951 default: 952 // This is unexpected 953 return nil, false, fmt.Errorf("unexpected line in hunk: %s", string(lineBytes)) 954 } 955 956 line := string(lineBytes) 957 if isFragment { 958 curFile.IsIncomplete = true 959 curFile.IsIncompleteLineTooLong = true 960 for isFragment { 961 lineBytes, isFragment, err = input.ReadLine() 962 if err != nil { 963 // Now by the definition of ReadLine this cannot be io.EOF 964 return lineBytes, isFragment, fmt.Errorf("unable to ReadLine: %w", err) 965 } 966 } 967 } 968 if len(line) > maxLineCharacters { 969 curFile.IsIncomplete = true 970 curFile.IsIncompleteLineTooLong = true 971 line = line[:maxLineCharacters] 972 } 973 curSection.Lines[len(curSection.Lines)-1].Content = line 974 975 // handle LFS 976 if line[1:] == lfs.MetaFileIdentifier { 977 curFileLFSPrefix = true 978 } else if curFileLFSPrefix && strings.HasPrefix(line[1:], lfs.MetaFileOidPrefix) { 979 oid := strings.TrimPrefix(line[1:], lfs.MetaFileOidPrefix) 980 if len(oid) == 64 { 981 m := &git_model.LFSMetaObject{Pointer: lfs.Pointer{Oid: oid}} 982 count, err := db.CountByBean(ctx, m) 983 984 if err == nil && count > 0 { 985 curFile.IsBin = true 986 curFile.IsLFSFile = true 987 curSection.Lines = nil 988 lastLeftIdx = -1 989 } 990 } 991 } 992 } 993} 994 995func createDiffFile(diff *Diff, line string) *DiffFile { 996 // The a/ and b/ filenames are the same unless rename/copy is involved. 997 // Especially, even for a creation or a deletion, /dev/null is not used 998 // in place of the a/ or b/ filenames. 999 // 1000 // When rename/copy is involved, file1 and file2 show the name of the 1001 // source file of the rename/copy and the name of the file that rename/copy 1002 // produces, respectively. 1003 // 1004 // Path names are quoted if necessary. 1005 // 1006 // This means that you should always be able to determine the file name even when there 1007 // there is potential ambiguity... 1008 // 1009 // but we can be simpler with our heuristics by just forcing git to prefix things nicely 1010 curFile := &DiffFile{ 1011 Index: len(diff.Files) + 1, 1012 Type: DiffFileChange, 1013 Sections: make([]*DiffSection, 0, 10), 1014 } 1015 1016 rd := strings.NewReader(line[len(cmdDiffHead):] + " ") 1017 curFile.Type = DiffFileChange 1018 var oldNameAmbiguity, newNameAmbiguity bool 1019 1020 curFile.OldName, oldNameAmbiguity = readFileName(rd) 1021 curFile.Name, newNameAmbiguity = readFileName(rd) 1022 if oldNameAmbiguity && newNameAmbiguity { 1023 curFile.IsAmbiguous = true 1024 // OK we should bet that the oldName and the newName are the same if they can be made to be same 1025 // So we need to start again ... 1026 if (len(line)-len(cmdDiffHead)-1)%2 == 0 { 1027 // diff --git a/b b/b b/b b/b b/b b/b 1028 // 1029 midpoint := (len(line) + len(cmdDiffHead) - 1) / 2 1030 newl, old := line[len(cmdDiffHead):midpoint], line[midpoint+1:] 1031 if len(newl) > 2 && len(old) > 2 && newl[2:] == old[2:] { 1032 curFile.OldName = old[2:] 1033 curFile.Name = old[2:] 1034 } 1035 } 1036 } 1037 1038 curFile.IsRenamed = curFile.Name != curFile.OldName 1039 return curFile 1040} 1041 1042func readFileName(rd *strings.Reader) (string, bool) { 1043 ambiguity := false 1044 var name string 1045 char, _ := rd.ReadByte() 1046 _ = rd.UnreadByte() 1047 if char == '"' { 1048 _, _ = fmt.Fscanf(rd, "%q ", &name) 1049 if len(name) == 0 { 1050 log.Error("Reader has no file name: reader=%+v", rd) 1051 return "", true 1052 } 1053 1054 if name[0] == '\\' { 1055 name = name[1:] 1056 } 1057 } else { 1058 // This technique is potentially ambiguous it may not be possible to uniquely identify the filenames from the diff line alone 1059 ambiguity = true 1060 _, _ = fmt.Fscanf(rd, "%s ", &name) 1061 char, _ := rd.ReadByte() 1062 _ = rd.UnreadByte() 1063 for char != 0 && char != '"' && char != 'b' { 1064 var suffix string 1065 _, _ = fmt.Fscanf(rd, "%s ", &suffix) 1066 name += " " + suffix 1067 char, _ = rd.ReadByte() 1068 _ = rd.UnreadByte() 1069 } 1070 } 1071 if len(name) < 2 { 1072 log.Error("Unable to determine name from reader: reader=%+v", rd) 1073 return "", true 1074 } 1075 return name[2:], ambiguity 1076} 1077 1078// DiffOptions represents the options for a DiffRange 1079type DiffOptions struct { 1080 BeforeCommitID string 1081 AfterCommitID string 1082 SkipTo string 1083 MaxLines int 1084 MaxLineCharacters int 1085 MaxFiles int 1086 WhitespaceBehavior git.TrustedCmdArgs 1087 DirectComparison bool 1088 FileOnly bool 1089} 1090 1091// GetDiff builds a Diff between two commits of a repository. 1092// Passing the empty string as beforeCommitID returns a diff from the parent commit. 1093// The whitespaceBehavior is either an empty string or a git flag 1094func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { 1095 repoPath := gitRepo.Path 1096 1097 var beforeCommit *git.Commit 1098 commit, err := gitRepo.GetCommit(opts.AfterCommitID) 1099 if err != nil { 1100 return nil, err 1101 } 1102 1103 cmdCtx, cmdCancel := context.WithCancel(ctx) 1104 defer cmdCancel() 1105 1106 cmdDiff := git.NewCommand(cmdCtx) 1107 objectFormat, err := gitRepo.GetObjectFormat() 1108 if err != nil { 1109 return nil, err 1110 } 1111 1112 if (len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == objectFormat.EmptyObjectID().String()) && commit.ParentCount() == 0 { 1113 cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M"). 1114 AddArguments(opts.WhitespaceBehavior...). 1115 AddDynamicArguments(objectFormat.EmptyTree().String()). 1116 AddDynamicArguments(opts.AfterCommitID) 1117 } else { 1118 actualBeforeCommitID := opts.BeforeCommitID 1119 if len(actualBeforeCommitID) == 0 { 1120 parentCommit, err := commit.Parent(0) 1121 if err != nil { 1122 return nil, err 1123 } 1124 actualBeforeCommitID = parentCommit.ID.String() 1125 } 1126 1127 cmdDiff.AddArguments("diff", "--src-prefix=\\a/", "--dst-prefix=\\b/", "-M"). 1128 AddArguments(opts.WhitespaceBehavior...). 1129 AddDynamicArguments(actualBeforeCommitID, opts.AfterCommitID) 1130 opts.BeforeCommitID = actualBeforeCommitID 1131 1132 beforeCommit, err = gitRepo.GetCommit(opts.BeforeCommitID) 1133 if err != nil { 1134 return nil, err 1135 } 1136 } 1137 1138 // In git 2.31, git diff learned --skip-to which we can use to shortcut skip to file 1139 // so if we are using at least this version of git we don't have to tell ParsePatch to do 1140 // the skipping for us 1141 parsePatchSkipToFile := opts.SkipTo 1142 if opts.SkipTo != "" && git.CheckGitVersionAtLeast("2.31") == nil { 1143 cmdDiff.AddOptionFormat("--skip-to=%s", opts.SkipTo) 1144 parsePatchSkipToFile = "" 1145 } 1146 1147 cmdDiff.AddDashesAndList(files...) 1148 1149 reader, writer := io.Pipe() 1150 defer func() { 1151 _ = reader.Close() 1152 _ = writer.Close() 1153 }() 1154 1155 go func() { 1156 stderr := &bytes.Buffer{} 1157 cmdDiff.SetDescription(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath)) 1158 if err := cmdDiff.Run(&git.RunOpts{ 1159 Timeout: time.Duration(setting.Git.Timeout.Default) * time.Second, 1160 Dir: repoPath, 1161 Stdout: writer, 1162 Stderr: stderr, 1163 }); err != nil && !git.IsErrCanceledOrKilled(err) { 1164 log.Error("error during GetDiff(git diff dir: %s): %v, stderr: %s", repoPath, err, stderr.String()) 1165 } 1166 1167 _ = writer.Close() 1168 }() 1169 1170 diff, err := ParsePatch(cmdCtx, opts.MaxLines, opts.MaxLineCharacters, opts.MaxFiles, reader, parsePatchSkipToFile) 1171 // Ensure the git process is killed if it didn't exit already 1172 cmdCancel() 1173 if err != nil { 1174 return nil, fmt.Errorf("unable to ParsePatch: %w", err) 1175 } 1176 diff.Start = opts.SkipTo 1177 1178 checker, err := gitRepo.GitAttributeChecker(opts.AfterCommitID, git.LinguistAttributes...) 1179 if err != nil { 1180 return nil, fmt.Errorf("unable to GitAttributeChecker: %w", err) 1181 } 1182 defer checker.Close() 1183 1184 for _, diffFile := range diff.Files { 1185 gotVendor := false 1186 gotGenerated := false 1187 1188 attrs, err := checker.CheckPath(diffFile.Name) 1189 if err != nil { 1190 log.Error("checker.CheckPath(%s) failed: %v", diffFile.Name, err) 1191 } else { 1192 vendored := attrs["linguist-vendored"].Bool() 1193 diffFile.IsVendored = vendored.Value() 1194 gotVendor = vendored.Has() 1195 1196 generated := attrs["linguist-generated"].Bool() 1197 diffFile.IsGenerated = generated.Value() 1198 gotGenerated = generated.Has() 1199 1200 diffFile.Language = cmp.Or( 1201 attrs["linguist-language"].String(), 1202 attrs["gitlab-language"].Prefix(), 1203 ) 1204 } 1205 1206 if !gotVendor { 1207 diffFile.IsVendored = analyze.IsVendor(diffFile.Name) 1208 } 1209 if !gotGenerated { 1210 diffFile.IsGenerated = analyze.IsGenerated(diffFile.Name) 1211 } 1212 1213 tailSection := diffFile.GetTailSection(gitRepo, beforeCommit, commit) 1214 if tailSection != nil { 1215 diffFile.Sections = append(diffFile.Sections, tailSection) 1216 } 1217 } 1218 1219 if opts.FileOnly { 1220 return diff, nil 1221 } 1222 1223 stats, err := GetPullDiffStats(gitRepo, opts) 1224 if err != nil { 1225 return nil, err 1226 } 1227 1228 diff.NumFiles, diff.TotalAddition, diff.TotalDeletion = stats.NumFiles, stats.TotalAddition, stats.TotalDeletion 1229 1230 return diff, nil 1231} 1232 1233type PullDiffStats struct { 1234 NumFiles, TotalAddition, TotalDeletion int 1235} 1236 1237// GetPullDiffStats 1238func GetPullDiffStats(gitRepo *git.Repository, opts *DiffOptions) (*PullDiffStats, error) { 1239 repoPath := gitRepo.Path 1240 1241 diff := &PullDiffStats{} 1242 1243 separator := "..." 1244 if opts.DirectComparison { 1245 separator = ".." 1246 } 1247 1248 objectFormat, err := gitRepo.GetObjectFormat() 1249 if err != nil { 1250 return nil, err 1251 } 1252 1253 diffPaths := []string{opts.BeforeCommitID + separator + opts.AfterCommitID} 1254 if len(opts.BeforeCommitID) == 0 || opts.BeforeCommitID == objectFormat.EmptyObjectID().String() { 1255 diffPaths = []string{objectFormat.EmptyTree().String(), opts.AfterCommitID} 1256 } 1257 1258 diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(gitRepo.Ctx, repoPath, nil, diffPaths...) 1259 if err != nil && strings.Contains(err.Error(), "no merge base") { 1260 // git >= 2.28 now returns an error if base and head have become unrelated. 1261 // previously it would return the results of git diff --shortstat base head so let's try that... 1262 diffPaths = []string{opts.BeforeCommitID, opts.AfterCommitID} 1263 diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(gitRepo.Ctx, repoPath, nil, diffPaths...) 1264 } 1265 if err != nil { 1266 return nil, err 1267 } 1268 1269 return diff, nil 1270} 1271 1272// SyncAndGetUserSpecificDiff is like GetDiff, except that user specific data such as which files the given user has already viewed on the given PR will also be set 1273// Additionally, the database asynchronously is updated if files have changed since the last review 1274func SyncAndGetUserSpecificDiff(ctx context.Context, userID int64, pull *issues_model.PullRequest, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) { 1275 diff, err := GetDiff(ctx, gitRepo, opts, files...) 1276 if err != nil { 1277 return nil, err 1278 } 1279 review, err := pull_model.GetNewestReviewState(ctx, userID, pull.ID) 1280 if err != nil || review == nil || review.UpdatedFiles == nil { 1281 return diff, err 1282 } 1283 1284 latestCommit := opts.AfterCommitID 1285 if latestCommit == "" { 1286 latestCommit = pull.HeadBranch // opts.AfterCommitID is preferred because it handles PRs from forks correctly and the branch name doesn't 1287 } 1288 1289 changedFiles, err := gitRepo.GetFilesChangedBetween(review.CommitSHA, latestCommit) 1290 // There are way too many possible errors. 1291 // Examples are various git errors such as the commit the review was based on was gc'ed and hence doesn't exist anymore as well as unrecoverable errors where we should serve a 500 response 1292 // Due to the current architecture and physical limitation of needing to compare explicit error messages, we can only choose one approach without the code getting ugly 1293 // For SOME of the errors such as the gc'ed commit, it would be best to mark all files as changed 1294 // But as that does not work for all potential errors, we simply mark all files as unchanged and drop the error which always works, even if not as good as possible 1295 if err != nil { 1296 log.Error("Could not get changed files between %s and %s for pull request %d in repo with path %s. Assuming no changes. Error: %w", review.CommitSHA, latestCommit, pull.Index, gitRepo.Path, err) 1297 } 1298 1299 filesChangedSinceLastDiff := make(map[string]pull_model.ViewedState) 1300outer: 1301 for _, diffFile := range diff.Files { 1302 fileViewedState := review.UpdatedFiles[diffFile.GetDiffFileName()] 1303 1304 // Check whether it was previously detected that the file has changed since the last review 1305 if fileViewedState == pull_model.HasChanged { 1306 diffFile.HasChangedSinceLastReview = true 1307 continue 1308 } 1309 1310 filename := diffFile.GetDiffFileName() 1311 1312 // Check explicitly whether the file has changed since the last review 1313 for _, changedFile := range changedFiles { 1314 diffFile.HasChangedSinceLastReview = filename == changedFile 1315 if diffFile.HasChangedSinceLastReview { 1316 filesChangedSinceLastDiff[filename] = pull_model.HasChanged 1317 continue outer // We don't want to check if the file is viewed here as that would fold the file, which is in this case unwanted 1318 } 1319 } 1320 // Check whether the file has already been viewed 1321 if fileViewedState == pull_model.Viewed { 1322 diffFile.IsViewed = true 1323 diff.NumViewedFiles++ 1324 } 1325 } 1326 1327 // Explicitly store files that have changed in the database, if any is present at all. 1328 // This has the benefit that the "Has Changed" attribute will be present as long as the user does not explicitly mark this file as viewed, so it will even survive a page reload after marking another file as viewed. 1329 // On the other hand, this means that even if a commit reverting an unseen change is committed, the file will still be seen as changed. 1330 if len(filesChangedSinceLastDiff) > 0 { 1331 err := pull_model.UpdateReviewState(ctx, review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff) 1332 if err != nil { 1333 log.Warn("Could not update review for user %d, pull %d, commit %s and the changed files %v: %v", review.UserID, review.PullID, review.CommitSHA, filesChangedSinceLastDiff, err) 1334 return nil, err 1335 } 1336 } 1337 1338 return diff, nil 1339} 1340 1341// CommentAsDiff returns c.Patch as *Diff 1342func CommentAsDiff(ctx context.Context, c *issues_model.Comment) (*Diff, error) { 1343 diff, err := ParsePatch(ctx, setting.Git.MaxGitDiffLines, 1344 setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch), "") 1345 if err != nil { 1346 log.Error("Unable to parse patch: %v", err) 1347 return nil, err 1348 } 1349 if len(diff.Files) == 0 { 1350 return nil, fmt.Errorf("no file found for comment ID: %d", c.ID) 1351 } 1352 secs := diff.Files[0].Sections 1353 if len(secs) == 0 { 1354 return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID) 1355 } 1356 return diff, nil 1357} 1358 1359// CommentMustAsDiff executes AsDiff and logs the error instead of returning 1360func CommentMustAsDiff(ctx context.Context, c *issues_model.Comment) *Diff { 1361 if c == nil { 1362 return nil 1363 } 1364 defer func() { 1365 if err := recover(); err != nil { 1366 log.Error("PANIC whilst retrieving diff for comment[%d] Error: %v\nStack: %s", c.ID, err, log.Stack(2)) 1367 } 1368 }() 1369 diff, err := CommentAsDiff(ctx, c) 1370 if err != nil { 1371 log.Warn("CommentMustAsDiff: %v", err) 1372 } 1373 return diff 1374} 1375 1376// GetWhitespaceFlag returns git diff flag for treating whitespaces 1377func GetWhitespaceFlag(whitespaceBehavior string) git.TrustedCmdArgs { 1378 whitespaceFlags := map[string]git.TrustedCmdArgs{ 1379 "ignore-all": {"-w"}, 1380 "ignore-change": {"-b"}, 1381 "ignore-eol": {"--ignore-space-at-eol"}, 1382 "show-all": nil, 1383 } 1384 if flag, ok := whitespaceFlags[whitespaceBehavior]; ok { 1385 return flag 1386 } 1387 return nil 1388}