// Copyright 2014 The Gogs Authors. All rights reserved.// Copyright 2019 The Gitea Authors. All rights reserved.// Use of this source code is governed by a MIT-style// license that can be found in the LICENSE file. package gitdiff import ( "bufio" "bytes" "fmt" "html" "html/template" "io" "io/ioutil" "net/url" "os" "os/exec" "regexp" "sort" "strconv" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "github.com/sergi/go-diff/diffmatchpatch" "github.com/unknwon/com" stdcharset "golang.org/x/net/html/charset" "golang.org/x/text/transform") // DiffLineType represents the type of a DiffLine.type DiffLineType uint8 // DiffLineType possible values.const ( DiffLinePlain DiffLineType = iota + 1 DiffLineAdd DiffLineDel DiffLineSection) // DiffFileType represents the type of a DiffFile.type DiffFileType uint8 // DiffFileType possible values.const ( DiffFileAdd DiffFileType = iota + 1 DiffFileChange DiffFileDel DiffFileRename) // DiffLineExpandDirection represents the DiffLineSection expand directiontype DiffLineExpandDirection uint8 // DiffLineExpandDirection possible values.const ( DiffLineExpandNone DiffLineExpandDirection = iota + 1 DiffLineExpandSingle DiffLineExpandUpDown DiffLineExpandUp DiffLineExpandDown) // DiffLine represents a line difference in a DiffSection.type DiffLine struct { LeftIdx int RightIdx int Type DiffLineType Content string Comments []*models.Comment SectionInfo *DiffLineSectionInfo} // DiffLineSectionInfo represents diff line section meta datatype DiffLineSectionInfo struct { Path string LastLeftIdx int LastRightIdx int LeftIdx int RightIdx int LeftHunkSize int RightHunkSize int} // BlobExceprtChunkSize represent max lines of excerptconst BlobExceprtChunkSize = 20 // GetType returns the type of a DiffLine.func (d *DiffLine) GetType() int { return int(d.Type)} // CanComment returns whether or not a line can get commentedfunc (d *DiffLine) CanComment() bool { return len(d.Comments) == 0 && d.Type != DiffLineSection} // GetCommentSide returns the comment side of the first comment, if not set returns empty stringfunc (d *DiffLine) GetCommentSide() string { if len(d.Comments) == 0 { return "" } return d.Comments[0].DiffSide()} // GetLineTypeMarker returns the line type markerfunc (d *DiffLine) GetLineTypeMarker() string { if strings.IndexByte(" +-", d.Content[0]) > -1 { return d.Content[0:1] } return ""} // GetBlobExcerptQuery builds query string to get blob excerptfunc (d *DiffLine) GetBlobExcerptQuery() string { query := fmt.Sprintf( "last_left=%d&last_right=%d&"+ "left=%d&right=%d&"+ "left_hunk_size=%d&right_hunk_size=%d&"+ "path=%s", d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx, d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx, d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize, url.QueryEscape(d.SectionInfo.Path)) return query} // GetExpandDirection gets DiffLineExpandDirectionfunc (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return DiffLineExpandNone } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { return DiffLineExpandUp } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExceprtChunkSize && d.SectionInfo.RightHunkSize > 0 { return DiffLineExpandUpDown } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { return DiffLineExpandDown } return DiffLineExpandSingle} func getDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { var ( leftLine int leftHunk int rightLine int righHunk int ) ss := strings.Split(line, "@@") ranges := strings.Split(ss[1][1:], " ") leftRange := strings.Split(ranges[0], ",") leftLine, _ = com.StrTo(leftRange[0][1:]).Int() if len(leftRange) > 1 { leftHunk, _ = com.StrTo(leftRange[1]).Int() } if len(ranges) > 1 { rightRange := strings.Split(ranges[1], ",") rightLine, _ = com.StrTo(rightRange[0]).Int() if len(rightRange) > 1 { righHunk, _ = com.StrTo(rightRange[1]).Int() } } else { log.Warn("Parse line number failed: %v", line) rightLine = leftLine righHunk = leftHunk } return &DiffLineSectionInfo{ Path: curFile.Name, LastLeftIdx: lastLeftIdx, LastRightIdx: lastRightIdx, LeftIdx: leftLine, RightIdx: rightLine, LeftHunkSize: leftHunk, RightHunkSize: righHunk, }} // escape a line's content or return <br> needed for copy/paste purposesfunc getLineContent(content string) string { if len(content) > 0 { return html.EscapeString(content) } return "<br>"} // DiffSection represents a section of a DiffFile.type DiffSection struct { Name string Lines []*DiffLine} var ( addedCodePrefix = []byte(`<span class="added-code">`) removedCodePrefix = []byte(`<span class="removed-code">`) codeTagSuffix = []byte(`</span>`)) func diffToHTML(diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML { buf := bytes.NewBuffer(nil) for i := range diffs { switch { case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: buf.Write(addedCodePrefix) buf.WriteString(getLineContent(diffs[i].Text)) buf.Write(codeTagSuffix) case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: buf.Write(removedCodePrefix) buf.WriteString(getLineContent(diffs[i].Text)) buf.Write(codeTagSuffix) case diffs[i].Type == diffmatchpatch.DiffEqual: buf.WriteString(getLineContent(diffs[i].Text)) } } return template.HTML(buf.Bytes())} // GetLine gets a specific line by type (add or del) and file line numberfunc (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine { var ( difference = 0 addCount = 0 delCount = 0 matchDiffLine *DiffLine ) LOOP: for _, diffLine := range diffSection.Lines { switch diffLine.Type { case DiffLineAdd: addCount++ case DiffLineDel: delCount++ default: if matchDiffLine != nil { break LOOP } difference = diffLine.RightIdx - diffLine.LeftIdx addCount = 0 delCount = 0 } switch lineType { case DiffLineDel: if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference { matchDiffLine = diffLine } case DiffLineAdd: if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference { matchDiffLine = diffLine } } } if addCount == delCount { return matchDiffLine } return nil} var diffMatchPatch = diffmatchpatch.New() func init() { diffMatchPatch.DiffEditCost = 100} // GetComputedInlineDiffFor computes inline diff for the given line.func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML { if setting.Git.DisableDiffHighlight { return template.HTML(getLineContent(diffLine.Content[1:])) } var ( compareDiffLine *DiffLine diff1 string diff2 string ) // try to find equivalent diff line. ignore, otherwise switch diffLine.Type { case DiffLineAdd: compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx) if compareDiffLine == nil { return template.HTML(getLineContent(diffLine.Content[1:])) } diff1 = compareDiffLine.Content diff2 = diffLine.Content case DiffLineDel: compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx) if compareDiffLine == nil { return template.HTML(getLineContent(diffLine.Content[1:])) } diff1 = diffLine.Content diff2 = compareDiffLine.Content default: if strings.IndexByte(" +-", diffLine.Content[0]) > -1 { return template.HTML(getLineContent(diffLine.Content[1:])) } return template.HTML(getLineContent(diffLine.Content)) } diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true) diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord) return diffToHTML(diffRecord, diffLine.Type)} // DiffFile represents a file diff.type DiffFile struct { Name string OldName string Index int Addition, Deletion int Type DiffFileType IsCreated bool IsDeleted bool IsBin bool IsLFSFile bool IsRenamed bool IsSubmodule bool Sections []*DiffSection IsIncomplete bool} // GetType returns type of diff file.func (diffFile *DiffFile) GetType() int { return int(diffFile.Type)} // GetHighlightClass returns highlight class for a filename.func (diffFile *DiffFile) GetHighlightClass() string { return highlight.FileNameToHighlightClass(diffFile.Name)} // GetTailSection creates a fake DiffLineSection if the last section is not the end of the filefunc (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection { if diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile { return nil } leftCommit, err := gitRepo.GetCommit(leftCommitID) if err != nil { return nil } rightCommit, err := gitRepo.GetCommit(rightCommitID) if err != nil { return nil } lastSection := diffFile.Sections[len(diffFile.Sections)-1] lastLine := lastSection.Lines[len(lastSection.Lines)-1] leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name) rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name) if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx { return nil } tailDiffLine := &DiffLine{ Type: DiffLineSection, Content: " ", SectionInfo: &DiffLineSectionInfo{ Path: diffFile.Name, LastLeftIdx: lastLine.LeftIdx, LastRightIdx: lastLine.RightIdx, LeftIdx: leftLineCount, RightIdx: rightLineCount, }} tailSection := &DiffSection{Lines: []*DiffLine{tailDiffLine}} return tailSection } func getCommitFileLineCount(commit *git.Commit, filePath string) int { blob, err := commit.GetBlobByPath(filePath) if err != nil { return 0 } lineCount, err := blob.GetBlobLineCount() if err != nil { return 0 } return lineCount} // Diff represents a difference between two git trees.type Diff struct { TotalAddition, TotalDeletion int Files []*DiffFile IsIncomplete bool} // LoadComments loads comments into each linefunc (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error { allComments, err := models.FetchCodeComments(issue, currentUser) if err != nil { return err } for _, file := range diff.Files { if lineCommits, ok := allComments[file.Name]; ok { for _, section := range file.Sections { for _, line := range section.Lines { if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { line.Comments = append(line.Comments, comments...) } if comments, ok := lineCommits[int64(line.RightIdx)]; ok { line.Comments = append(line.Comments, comments...) } sort.SliceStable(line.Comments, func(i, j int) bool { return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix }) } } } } return nil} // NumFiles returns number of files changes in a diff.func (diff *Diff) NumFiles() int { return len(diff.Files)} // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9]var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`) func isHeader(lof string) bool { return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++")} // CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown// it also recalculates hunks and adds the appropriate headers to the new diff.// Warning: Only one-file diffs are allowed.func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string { if line == 0 || numbersOfLine == 0 { // no line or num of lines => no diff return "" } scanner := bufio.NewScanner(originalDiff) hunk := make([]string, 0) // begin is the start of the hunk containing searched line // end is the end of the hunk ... // currentLine is the line number on the side of the searched line (differentiated by old) // otherLine is the line number on the opposite side of the searched line (differentiated by old) var begin, end, currentLine, otherLine int64 var headerLines int for scanner.Scan() { lof := scanner.Text() // Add header to enable parsing if isHeader(lof) { hunk = append(hunk, lof) headerLines++ } if currentLine > line { break } // Detect "hunk" with contains commented lof if strings.HasPrefix(lof, "@@") { // Already got our hunk. End of hunk detected! if len(hunk) > headerLines { break } // A map with named groups of our regex to recognize them later more easily submatches := hunkRegex.FindStringSubmatch(lof) groups := make(map[string]string) for i, name := range hunkRegex.SubexpNames() { if i != 0 && name != "" { groups[name] = submatches[i] } } if old { begin = com.StrTo(groups["beginOld"]).MustInt64() end = com.StrTo(groups["endOld"]).MustInt64() // init otherLine with begin of opposite side otherLine = com.StrTo(groups["beginNew"]).MustInt64() } else { begin = com.StrTo(groups["beginNew"]).MustInt64() if groups["endNew"] != "" { end = com.StrTo(groups["endNew"]).MustInt64() } else { end = 0 } // init otherLine with begin of opposite side otherLine = com.StrTo(groups["beginOld"]).MustInt64() } end += begin // end is for real only the number of lines in hunk // lof is between begin and end if begin <= line && end >= line { hunk = append(hunk, lof) currentLine = begin continue } } else if len(hunk) > headerLines { hunk = append(hunk, lof) // Count lines in contextLines 1–500 of 976. Load more
// Copyright 2014 The Gogs Authors. All rights reserved. // Copyright 2019 The Gitea Authors. All rights reserved. // Use of this source code is governed by a MIT-style // license that can be found in the LICENSE file. package gitdiff import ( "bufio" "bytes" "fmt" "html" "html/template" "io" "io/ioutil" "net/url" "os" "os/exec" "regexp" "sort" "strconv" "strings" "code.gitea.io/gitea/models" "code.gitea.io/gitea/modules/charset" "code.gitea.io/gitea/modules/git" "code.gitea.io/gitea/modules/highlight" "code.gitea.io/gitea/modules/log" "code.gitea.io/gitea/modules/process" "code.gitea.io/gitea/modules/setting" "github.com/sergi/go-diff/diffmatchpatch" "github.com/unknwon/com" stdcharset "golang.org/x/net/html/charset" "golang.org/x/text/transform" ) // DiffLineType represents the type of a DiffLine. type DiffLineType uint8 // DiffLineType possible values. const ( DiffLinePlain DiffLineType = iota + 1 DiffLineAdd DiffLineDel DiffLineSection ) // DiffFileType represents the type of a DiffFile. type DiffFileType uint8 // DiffFileType possible values. const ( DiffFileAdd DiffFileType = iota + 1 DiffFileChange DiffFileDel DiffFileRename ) // DiffLineExpandDirection represents the DiffLineSection expand direction type DiffLineExpandDirection uint8 // DiffLineExpandDirection possible values. const ( DiffLineExpandNone DiffLineExpandDirection = iota + 1 DiffLineExpandSingle DiffLineExpandUpDown DiffLineExpandUp DiffLineExpandDown ) // DiffLine represents a line difference in a DiffSection. type DiffLine struct { LeftIdx int RightIdx int Type DiffLineType Content string Comments []*models.Comment SectionInfo *DiffLineSectionInfo } // DiffLineSectionInfo represents diff line section meta data type DiffLineSectionInfo struct { Path string LastLeftIdx int LastRightIdx int LeftIdx int RightIdx int LeftHunkSize int RightHunkSize int } // BlobExceprtChunkSize represent max lines of excerpt const BlobExceprtChunkSize = 20 // GetType returns the type of a DiffLine. func (d *DiffLine) GetType() int { return int(d.Type) } // CanComment returns whether or not a line can get commented func (d *DiffLine) CanComment() bool { return len(d.Comments) == 0 && d.Type != DiffLineSection } // GetCommentSide returns the comment side of the first comment, if not set returns empty string func (d *DiffLine) GetCommentSide() string { if len(d.Comments) == 0 { return "" } return d.Comments[0].DiffSide() } // GetLineTypeMarker returns the line type marker func (d *DiffLine) GetLineTypeMarker() string { if strings.IndexByte(" +-", d.Content[0]) > -1 { return d.Content[0:1] } return "" } // GetBlobExcerptQuery builds query string to get blob excerpt func (d *DiffLine) GetBlobExcerptQuery() string { query := fmt.Sprintf( "last_left=%d&last_right=%d&"+ "left=%d&right=%d&"+ "left_hunk_size=%d&right_hunk_size=%d&"+ "path=%s", d.SectionInfo.LastLeftIdx, d.SectionInfo.LastRightIdx, d.SectionInfo.LeftIdx, d.SectionInfo.RightIdx, d.SectionInfo.LeftHunkSize, d.SectionInfo.RightHunkSize, url.QueryEscape(d.SectionInfo.Path)) return query } // GetExpandDirection gets DiffLineExpandDirection func (d *DiffLine) GetExpandDirection() DiffLineExpandDirection { if d.Type != DiffLineSection || d.SectionInfo == nil || d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx <= 1 { return DiffLineExpandNone } if d.SectionInfo.LastLeftIdx <= 0 && d.SectionInfo.LastRightIdx <= 0 { return DiffLineExpandUp } else if d.SectionInfo.RightIdx-d.SectionInfo.LastRightIdx > BlobExceprtChunkSize && d.SectionInfo.RightHunkSize > 0 { return DiffLineExpandUpDown } else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 { return DiffLineExpandDown } return DiffLineExpandSingle } func getDiffLineSectionInfo(curFile *DiffFile, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo { var ( leftLine int leftHunk int rightLine int righHunk int ) ss := strings.Split(line, "@@") ranges := strings.Split(ss[1][1:], " ") leftRange := strings.Split(ranges[0], ",") leftLine, _ = com.StrTo(leftRange[0][1:]).Int() if len(leftRange) > 1 { leftHunk, _ = com.StrTo(leftRange[1]).Int() } if len(ranges) > 1 { rightRange := strings.Split(ranges[1], ",") rightLine, _ = com.StrTo(rightRange[0]).Int() if len(rightRange) > 1 { righHunk, _ = com.StrTo(rightRange[1]).Int() } } else { log.Warn("Parse line number failed: %v", line) rightLine = leftLine righHunk = leftHunk } return &DiffLineSectionInfo{ Path: curFile.Name, LastLeftIdx: lastLeftIdx, LastRightIdx: lastRightIdx, LeftIdx: leftLine, RightIdx: rightLine, LeftHunkSize: leftHunk, RightHunkSize: righHunk, } } // escape a line's content or return <br> needed for copy/paste purposes func getLineContent(content string) string { if len(content) > 0 { return html.EscapeString(content) } return "<br>" } // DiffSection represents a section of a DiffFile. type DiffSection struct { Name string Lines []*DiffLine } var ( addedCodePrefix = []byte(`<span class="added-code">`) removedCodePrefix = []byte(`<span class="removed-code">`) codeTagSuffix = []byte(`</span>`) ) func diffToHTML(diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML { buf := bytes.NewBuffer(nil) for i := range diffs { switch { case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: buf.Write(addedCodePrefix) buf.WriteString(getLineContent(diffs[i].Text)) buf.Write(codeTagSuffix) case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: buf.Write(removedCodePrefix) buf.WriteString(getLineContent(diffs[i].Text)) buf.Write(codeTagSuffix) case diffs[i].Type == diffmatchpatch.DiffEqual: buf.WriteString(getLineContent(diffs[i].Text)) } } return template.HTML(buf.Bytes()) } // GetLine gets a specific line by type (add or del) and file line number func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine { var ( difference = 0 addCount = 0 delCount = 0 matchDiffLine *DiffLine ) LOOP: for _, diffLine := range diffSection.Lines { switch diffLine.Type { case DiffLineAdd: addCount++ case DiffLineDel: delCount++ default: if matchDiffLine != nil { break LOOP } difference = diffLine.RightIdx - diffLine.LeftIdx addCount = 0 delCount = 0 } switch lineType { case DiffLineDel: if diffLine.RightIdx == 0 && diffLine.LeftIdx == idx-difference { matchDiffLine = diffLine } case DiffLineAdd: if diffLine.LeftIdx == 0 && diffLine.RightIdx == idx+difference { matchDiffLine = diffLine } } } if addCount == delCount { return matchDiffLine } return nil } var diffMatchPatch = diffmatchpatch.New() func init() { diffMatchPatch.DiffEditCost = 100 } // GetComputedInlineDiffFor computes inline diff for the given line. func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML { if setting.Git.DisableDiffHighlight { return template.HTML(getLineContent(diffLine.Content[1:])) } var ( compareDiffLine *DiffLine diff1 string diff2 string ) // try to find equivalent diff line. ignore, otherwise switch diffLine.Type { case DiffLineAdd: compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx) if compareDiffLine == nil { return template.HTML(getLineContent(diffLine.Content[1:])) } diff1 = compareDiffLine.Content diff2 = diffLine.Content case DiffLineDel: compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx) if compareDiffLine == nil { return template.HTML(getLineContent(diffLine.Content[1:])) } diff1 = diffLine.Content diff2 = compareDiffLine.Content default: if strings.IndexByte(" +-", diffLine.Content[0]) > -1 { return template.HTML(getLineContent(diffLine.Content[1:])) } return template.HTML(getLineContent(diffLine.Content)) } diffRecord := diffMatchPatch.DiffMain(diff1[1:], diff2[1:], true) diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord) return diffToHTML(diffRecord, diffLine.Type) } // DiffFile represents a file diff. type DiffFile struct { Name string OldName string Index int Addition, Deletion int Type DiffFileType IsCreated bool IsDeleted bool IsBin bool IsLFSFile bool IsRenamed bool IsSubmodule bool Sections []*DiffSection IsIncomplete bool } // GetType returns type of diff file. func (diffFile *DiffFile) GetType() int { return int(diffFile.Type) } // GetHighlightClass returns highlight class for a filename. func (diffFile *DiffFile) GetHighlightClass() string { return highlight.FileNameToHighlightClass(diffFile.Name) } // GetTailSection creates a fake DiffLineSection if the last section is not the end of the file func (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommitID, rightCommitID string) *DiffSection { if diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile { return nil } leftCommit, err := gitRepo.GetCommit(leftCommitID) if err != nil { return nil } rightCommit, err := gitRepo.GetCommit(rightCommitID) if err != nil { return nil } lastSection := diffFile.Sections[len(diffFile.Sections)-1] lastLine := lastSection.Lines[len(lastSection.Lines)-1] leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name) rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name) if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx { return nil } tailDiffLine := &DiffLine{ Type: DiffLineSection, Content: " ", SectionInfo: &DiffLineSectionInfo{ Path: diffFile.Name, LastLeftIdx: lastLine.LeftIdx, LastRightIdx: lastLine.RightIdx, LeftIdx: leftLineCount, RightIdx: rightLineCount, }} tailSection := &DiffSection{Lines: []*DiffLine{tailDiffLine}} return tailSection } func getCommitFileLineCount(commit *git.Commit, filePath string) int { blob, err := commit.GetBlobByPath(filePath) if err != nil { return 0 } lineCount, err := blob.GetBlobLineCount() if err != nil { return 0 } return lineCount } // Diff represents a difference between two git trees. type Diff struct { TotalAddition, TotalDeletion int Files []*DiffFile IsIncomplete bool } // LoadComments loads comments into each line func (diff *Diff) LoadComments(issue *models.Issue, currentUser *models.User) error { allComments, err := models.FetchCodeComments(issue, currentUser) if err != nil { return err } for _, file := range diff.Files { if lineCommits, ok := allComments[file.Name]; ok { for _, section := range file.Sections { for _, line := range section.Lines { if comments, ok := lineCommits[int64(line.LeftIdx*-1)]; ok { line.Comments = append(line.Comments, comments...) } if comments, ok := lineCommits[int64(line.RightIdx)]; ok { line.Comments = append(line.Comments, comments...) } sort.SliceStable(line.Comments, func(i, j int) bool { return line.Comments[i].CreatedUnix < line.Comments[j].CreatedUnix }) } } } } return nil } // NumFiles returns number of files changes in a diff. func (diff *Diff) NumFiles() int { return len(diff.Files) } // Example: @@ -1,8 +1,9 @@ => [..., 1, 8, 1, 9] var hunkRegex = regexp.MustCompile(`^@@ -(?P<beginOld>[0-9]+)(,(?P<endOld>[0-9]+))? \+(?P<beginNew>[0-9]+)(,(?P<endNew>[0-9]+))? @@`) func isHeader(lof string) bool { return strings.HasPrefix(lof, cmdDiffHead) || strings.HasPrefix(lof, "---") || strings.HasPrefix(lof, "+++") } // CutDiffAroundLine cuts a diff of a file in way that only the given line + numberOfLine above it will be shown // it also recalculates hunks and adds the appropriate headers to the new diff. // Warning: Only one-file diffs are allowed. func CutDiffAroundLine(originalDiff io.Reader, line int64, old bool, numbersOfLine int) string { if line == 0 || numbersOfLine == 0 { // no line or num of lines => no diff return "" } scanner := bufio.NewScanner(originalDiff) hunk := make([]string, 0) // begin is the start of the hunk containing searched line // end is the end of the hunk ... // currentLine is the line number on the side of the searched line (differentiated by old) // otherLine is the line number on the opposite side of the searched line (differentiated by old) var begin, end, currentLine, otherLine int64 var headerLines int for scanner.Scan() { lof := scanner.Text() // Add header to enable parsing if isHeader(lof) { hunk = append(hunk, lof) headerLines++ } if currentLine > line { break } // Detect "hunk" with contains commented lof if strings.HasPrefix(lof, "@@") { // Already got our hunk. End of hunk detected! if len(hunk) > headerLines { break } // A map with named groups of our regex to recognize them later more easily submatches := hunkRegex.FindStringSubmatch(lof) groups := make(map[string]string) for i, name := range hunkRegex.SubexpNames() { if i != 0 && name != "" { groups[name] = submatches[i] } } if old { begin = com.StrTo(groups["beginOld"]).MustInt64() end = com.StrTo(groups["endOld"]).MustInt64() // init otherLine with begin of opposite side otherLine = com.StrTo(groups["beginNew"]).MustInt64() } else { begin = com.StrTo(groups["beginNew"]).MustInt64() if groups["endNew"] != "" { end = com.StrTo(groups["endNew"]).MustInt64() } else { end = 0 } // init otherLine with begin of opposite side otherLine = com.StrTo(groups["beginOld"]).MustInt64() } end += begin // end is for real only the number of lines in hunk // lof is between begin and end if begin <= line && end >= line { hunk = append(hunk, lof) currentLine = begin continue } } else if len(hunk) > headerLines { hunk = append(hunk, lof) // Count lines in context