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}