96969ddec8
Following further testing it has become apparent that the diff line cannot be used to determine filenames for diffs with any sort of predictability the answer therefore is to use the other lines that are provided with a diff Fix #12768 Signed-off-by: Andrew Thornton <art27@cantab.net>
851 lines
24 KiB
Go
851 lines
24 KiB
Go
// 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"
|
|
"context"
|
|
"fmt"
|
|
"html/template"
|
|
"io"
|
|
"io/ioutil"
|
|
"net/url"
|
|
"os"
|
|
"os/exec"
|
|
"regexp"
|
|
"sort"
|
|
"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"
|
|
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
|
|
DiffFileCopy
|
|
)
|
|
|
|
// 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
|
|
}
|
|
|
|
// BlobExcerptChunkSize represent max lines of excerpt
|
|
const BlobExcerptChunkSize = 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 > BlobExcerptChunkSize && d.SectionInfo.RightHunkSize > 0 {
|
|
return DiffLineExpandUpDown
|
|
} else if d.SectionInfo.LeftHunkSize <= 0 && d.SectionInfo.RightHunkSize <= 0 {
|
|
return DiffLineExpandDown
|
|
}
|
|
return DiffLineExpandSingle
|
|
}
|
|
|
|
func getDiffLineSectionInfo(treePath, line string, lastLeftIdx, lastRightIdx int) *DiffLineSectionInfo {
|
|
leftLine, leftHunk, rightLine, righHunk := git.ParseDiffHunkString(line)
|
|
|
|
return &DiffLineSectionInfo{
|
|
Path: treePath,
|
|
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 content
|
|
}
|
|
return "\n"
|
|
}
|
|
|
|
// DiffSection represents a section of a DiffFile.
|
|
type DiffSection struct {
|
|
FileName string
|
|
Name string
|
|
Lines []*DiffLine
|
|
}
|
|
|
|
var (
|
|
addedCodePrefix = []byte(`<span class="added-code">`)
|
|
removedCodePrefix = []byte(`<span class="removed-code">`)
|
|
codeTagSuffix = []byte(`</span>`)
|
|
)
|
|
var addSpanRegex = regexp.MustCompile(`<span [class="[a-z]*]*$`)
|
|
|
|
func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineType) template.HTML {
|
|
buf := bytes.NewBuffer(nil)
|
|
var addSpan string
|
|
for i := range diffs {
|
|
switch {
|
|
case diffs[i].Type == diffmatchpatch.DiffEqual:
|
|
// Looking for the case where our 3rd party diff library previously detected a string difference
|
|
// in the middle of a span class because we highlight them first. This happens when added/deleted code
|
|
// also changes the chroma class name, either partially or fully. If found, just move the openining span code forward into the next section
|
|
// see TestDiffToHTML for examples
|
|
if len(addSpan) > 0 {
|
|
diffs[i].Text = addSpan + diffs[i].Text
|
|
addSpan = ""
|
|
}
|
|
m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
|
|
if m != nil {
|
|
addSpan = diffs[i].Text[m[0]:m[1]]
|
|
buf.WriteString(strings.TrimSuffix(diffs[i].Text, addSpan))
|
|
} else {
|
|
addSpan = ""
|
|
buf.WriteString(getLineContent(diffs[i].Text))
|
|
}
|
|
case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
|
|
if len(addSpan) > 0 {
|
|
diffs[i].Text = addSpan + diffs[i].Text
|
|
addSpan = ""
|
|
}
|
|
// Print existing closing span first before opening added-code span so it doesn't unintentionally close it
|
|
if strings.HasPrefix(diffs[i].Text, "</span>") {
|
|
buf.WriteString("</span>")
|
|
diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
|
|
}
|
|
m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
|
|
if m != nil {
|
|
addSpan = diffs[i].Text[m[0]:m[1]]
|
|
diffs[i].Text = strings.TrimSuffix(diffs[i].Text, addSpan)
|
|
}
|
|
buf.Write(addedCodePrefix)
|
|
buf.WriteString(getLineContent(diffs[i].Text))
|
|
buf.Write(codeTagSuffix)
|
|
case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
|
|
if len(addSpan) > 0 {
|
|
diffs[i].Text = addSpan + diffs[i].Text
|
|
addSpan = ""
|
|
}
|
|
if strings.HasPrefix(diffs[i].Text, "</span>") {
|
|
buf.WriteString("</span>")
|
|
diffs[i].Text = strings.TrimPrefix(diffs[i].Text, "</span>")
|
|
}
|
|
m := addSpanRegex.FindStringSubmatchIndex(diffs[i].Text)
|
|
if m != nil {
|
|
addSpan = diffs[i].Text[m[0]:m[1]]
|
|
diffs[i].Text = strings.TrimSuffix(diffs[i].Text, addSpan)
|
|
}
|
|
buf.Write(removedCodePrefix)
|
|
buf.WriteString(getLineContent(diffs[i].Text))
|
|
buf.Write(codeTagSuffix)
|
|
}
|
|
}
|
|
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 DiffLineSection:
|
|
return template.HTML(getLineContent(diffLine.Content[1:]))
|
|
case DiffLineAdd:
|
|
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
|
|
if compareDiffLine == nil {
|
|
return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
|
|
}
|
|
diff1 = compareDiffLine.Content
|
|
diff2 = diffLine.Content
|
|
case DiffLineDel:
|
|
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
|
|
if compareDiffLine == nil {
|
|
return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
|
|
}
|
|
diff1 = diffLine.Content
|
|
diff2 = compareDiffLine.Content
|
|
default:
|
|
if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
|
|
return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content[1:]))
|
|
}
|
|
return template.HTML(highlight.Code(diffSection.FileName, diffLine.Content))
|
|
}
|
|
|
|
diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
|
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
|
return diffToHTML(diffSection.FileName, 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)
|
|
}
|
|
|
|
// 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 len(diffFile.Sections) == 0 || 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{FileName: diffFile.Name, 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 {
|
|
NumFiles, 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
|
|
}
|
|
|
|
const cmdDiffHead = "diff --git "
|
|
|
|
// ParsePatch builds a Diff object from a io.Reader and some
|
|
// parameters.
|
|
// TODO: move this function to gogits/git-module
|
|
func ParsePatch(maxLines, maxLineCharacters, maxFiles int, reader io.Reader) (*Diff, error) {
|
|
var (
|
|
diff = &Diff{Files: make([]*DiffFile, 0)}
|
|
curFile = &DiffFile{}
|
|
curSection = &DiffSection{
|
|
Lines: make([]*DiffLine, 0, 10),
|
|
}
|
|
|
|
leftLine, rightLine int
|
|
lineCount int
|
|
curFileLinesCount int
|
|
curFileLFSPrefix bool
|
|
)
|
|
|
|
input := bufio.NewReader(reader)
|
|
isEOF := false
|
|
for !isEOF {
|
|
var linebuf bytes.Buffer
|
|
for {
|
|
b, err := input.ReadByte()
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
isEOF = true
|
|
break
|
|
} else {
|
|
return nil, fmt.Errorf("ReadByte: %v", err)
|
|
}
|
|
}
|
|
if b == '\n' {
|
|
break
|
|
}
|
|
if linebuf.Len() < maxLineCharacters {
|
|
linebuf.WriteByte(b)
|
|
} else if linebuf.Len() == maxLineCharacters {
|
|
curFile.IsIncomplete = true
|
|
}
|
|
}
|
|
line := linebuf.String()
|
|
|
|
if strings.HasPrefix(line, "--- ") {
|
|
if line[4] == '"' {
|
|
fmt.Sscanf(line[4:], "%q", &curFile.OldName)
|
|
} else {
|
|
curFile.OldName = line[4:]
|
|
if strings.Contains(curFile.OldName, " ") {
|
|
// Git adds a terminal \t if there is a space in the name
|
|
curFile.OldName = curFile.OldName[:len(curFile.OldName)-1]
|
|
}
|
|
}
|
|
if curFile.OldName[0:2] == "a/" {
|
|
curFile.OldName = curFile.OldName[2:]
|
|
}
|
|
continue
|
|
} else if strings.HasPrefix(line, "+++ ") {
|
|
if line[4] == '"' {
|
|
fmt.Sscanf(line[4:], "%q", &curFile.Name)
|
|
} else {
|
|
curFile.Name = line[4:]
|
|
if strings.Contains(curFile.Name, " ") {
|
|
// Git adds a terminal \t if there is a space in the name
|
|
curFile.Name = curFile.Name[:len(curFile.Name)-1]
|
|
}
|
|
}
|
|
if curFile.Name[0:2] == "b/" {
|
|
curFile.Name = curFile.Name[2:]
|
|
}
|
|
curFile.IsRenamed = (curFile.Name != curFile.OldName) && !(curFile.IsCreated || curFile.IsDeleted)
|
|
if curFile.IsDeleted {
|
|
curFile.Name = curFile.OldName
|
|
curFile.OldName = ""
|
|
} else if curFile.IsCreated {
|
|
curFile.OldName = ""
|
|
}
|
|
continue
|
|
} else if len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
if strings.HasPrefix(line, "+++") || strings.HasPrefix(line, "---") || len(line) == 0 {
|
|
continue
|
|
}
|
|
|
|
trimLine := strings.Trim(line, "+- ")
|
|
|
|
if trimLine == models.LFSMetaFileIdentifier {
|
|
curFileLFSPrefix = true
|
|
}
|
|
|
|
if curFileLFSPrefix && strings.HasPrefix(trimLine, models.LFSMetaFileOidPrefix) {
|
|
oid := strings.TrimPrefix(trimLine, models.LFSMetaFileOidPrefix)
|
|
|
|
if len(oid) == 64 {
|
|
m := &models.LFSMetaObject{Oid: oid}
|
|
count, err := models.Count(m)
|
|
|
|
if err == nil && count > 0 {
|
|
curFile.IsBin = true
|
|
curFile.IsLFSFile = true
|
|
curSection.Lines = nil
|
|
}
|
|
}
|
|
}
|
|
|
|
curFileLinesCount++
|
|
lineCount++
|
|
|
|
// Diff data too large, we only show the first about maxLines lines
|
|
if curFileLinesCount >= maxLines {
|
|
curFile.IsIncomplete = true
|
|
}
|
|
switch {
|
|
case line[0] == ' ':
|
|
diffLine := &DiffLine{Type: DiffLinePlain, Content: line, LeftIdx: leftLine, RightIdx: rightLine}
|
|
leftLine++
|
|
rightLine++
|
|
curSection.Lines = append(curSection.Lines, diffLine)
|
|
curSection.FileName = curFile.Name
|
|
continue
|
|
case line[0] == '@':
|
|
curSection = &DiffSection{}
|
|
curFile.Sections = append(curFile.Sections, curSection)
|
|
lineSectionInfo := getDiffLineSectionInfo(curFile.Name, line, leftLine-1, rightLine-1)
|
|
diffLine := &DiffLine{
|
|
Type: DiffLineSection,
|
|
Content: line,
|
|
SectionInfo: lineSectionInfo,
|
|
}
|
|
curSection.Lines = append(curSection.Lines, diffLine)
|
|
curSection.FileName = curFile.Name
|
|
// update line number.
|
|
leftLine = lineSectionInfo.LeftIdx
|
|
rightLine = lineSectionInfo.RightIdx
|
|
continue
|
|
case line[0] == '+':
|
|
curFile.Addition++
|
|
diff.TotalAddition++
|
|
diffLine := &DiffLine{Type: DiffLineAdd, Content: line, RightIdx: rightLine}
|
|
rightLine++
|
|
curSection.Lines = append(curSection.Lines, diffLine)
|
|
curSection.FileName = curFile.Name
|
|
continue
|
|
case line[0] == '-':
|
|
curFile.Deletion++
|
|
diff.TotalDeletion++
|
|
diffLine := &DiffLine{Type: DiffLineDel, Content: line, LeftIdx: leftLine}
|
|
if leftLine > 0 {
|
|
leftLine++
|
|
}
|
|
curSection.Lines = append(curSection.Lines, diffLine)
|
|
curSection.FileName = curFile.Name
|
|
case strings.HasPrefix(line, "Binary"):
|
|
curFile.IsBin = true
|
|
continue
|
|
}
|
|
|
|
// Get new file.
|
|
if strings.HasPrefix(line, cmdDiffHead) {
|
|
if len(diff.Files) >= maxFiles {
|
|
diff.IsIncomplete = true
|
|
_, err := io.Copy(ioutil.Discard, reader)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("Copy: %v", err)
|
|
}
|
|
break
|
|
}
|
|
|
|
curFile = &DiffFile{
|
|
Index: len(diff.Files) + 1,
|
|
Type: DiffFileChange,
|
|
Sections: make([]*DiffSection, 0, 10),
|
|
}
|
|
diff.Files = append(diff.Files, curFile)
|
|
curFileLinesCount = 0
|
|
leftLine = 1
|
|
rightLine = 1
|
|
curFileLFSPrefix = false
|
|
|
|
// Check file diff type and is submodule.
|
|
loop:
|
|
for {
|
|
line, err := input.ReadString('\n')
|
|
if err != nil {
|
|
if err == io.EOF {
|
|
isEOF = true
|
|
} else {
|
|
return nil, fmt.Errorf("ReadString: %v", err)
|
|
}
|
|
}
|
|
|
|
if curFile.Type != DiffFileRename {
|
|
switch {
|
|
case strings.HasPrefix(line, "new file"):
|
|
curFile.Type = DiffFileAdd
|
|
curFile.IsCreated = true
|
|
case strings.HasPrefix(line, "deleted"):
|
|
curFile.Type = DiffFileDel
|
|
curFile.IsDeleted = true
|
|
case strings.HasPrefix(line, "index"):
|
|
curFile.Type = DiffFileChange
|
|
case strings.HasPrefix(line, "similarity index 100%"):
|
|
curFile.Type = DiffFileRename
|
|
}
|
|
if curFile.Type > 0 && curFile.Type != DiffFileRename {
|
|
if strings.HasSuffix(line, " 160000\n") {
|
|
curFile.IsSubmodule = true
|
|
}
|
|
break
|
|
}
|
|
} else {
|
|
switch {
|
|
case strings.HasPrefix(line, "rename from "):
|
|
if line[12] == '"' {
|
|
fmt.Sscanf(line[12:], "%q", &curFile.OldName)
|
|
} else {
|
|
curFile.OldName = line[12:]
|
|
curFile.OldName = curFile.OldName[:len(curFile.OldName)-1]
|
|
}
|
|
case strings.HasPrefix(line, "rename to "):
|
|
if line[10] == '"' {
|
|
fmt.Sscanf(line[10:], "%q", &curFile.Name)
|
|
} else {
|
|
curFile.Name = line[10:]
|
|
curFile.Name = curFile.Name[:len(curFile.Name)-1]
|
|
}
|
|
curFile.IsRenamed = true
|
|
break loop
|
|
case strings.HasPrefix(line, "copy from "):
|
|
if line[10] == '"' {
|
|
fmt.Sscanf(line[10:], "%q", &curFile.OldName)
|
|
} else {
|
|
curFile.OldName = line[10:]
|
|
curFile.OldName = curFile.OldName[:len(curFile.OldName)-1]
|
|
}
|
|
case strings.HasPrefix(line, "copy to "):
|
|
if line[8] == '"' {
|
|
fmt.Sscanf(line[8:], "%q", &curFile.Name)
|
|
} else {
|
|
curFile.Name = line[8:]
|
|
curFile.Name = curFile.Name[:len(curFile.Name)-1]
|
|
}
|
|
curFile.IsRenamed = true
|
|
curFile.Type = DiffFileCopy
|
|
break loop
|
|
default:
|
|
if strings.HasSuffix(line, " 160000\n") {
|
|
curFile.IsSubmodule = true
|
|
} else {
|
|
break loop
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// FIXME: detect encoding while parsing.
|
|
var buf bytes.Buffer
|
|
for _, f := range diff.Files {
|
|
buf.Reset()
|
|
for _, sec := range f.Sections {
|
|
for _, l := range sec.Lines {
|
|
buf.WriteString(l.Content)
|
|
buf.WriteString("\n")
|
|
}
|
|
}
|
|
charsetLabel, err := charset.DetectEncoding(buf.Bytes())
|
|
if charsetLabel != "UTF-8" && err == nil {
|
|
encoding, _ := stdcharset.Lookup(charsetLabel)
|
|
if encoding != nil {
|
|
d := encoding.NewDecoder()
|
|
for _, sec := range f.Sections {
|
|
for _, l := range sec.Lines {
|
|
if c, _, err := transform.String(d, l.Content); err == nil {
|
|
l.Content = c
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
diff.NumFiles = len(diff.Files)
|
|
return diff, nil
|
|
}
|
|
|
|
// GetDiffRange builds a Diff between two commits of a repository.
|
|
// passing the empty string as beforeCommitID returns a diff from the
|
|
// parent commit.
|
|
func GetDiffRange(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
|
return GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID, maxLines, maxLineCharacters, maxFiles, "")
|
|
}
|
|
|
|
// GetDiffRangeWithWhitespaceBehavior builds a Diff between two commits of a repository.
|
|
// Passing the empty string as beforeCommitID returns a diff from the parent commit.
|
|
// The whitespaceBehavior is either an empty string or a git flag
|
|
func GetDiffRangeWithWhitespaceBehavior(repoPath, beforeCommitID, afterCommitID string, maxLines, maxLineCharacters, maxFiles int, whitespaceBehavior string) (*Diff, error) {
|
|
gitRepo, err := git.OpenRepository(repoPath)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
defer gitRepo.Close()
|
|
|
|
commit, err := gitRepo.GetCommit(afterCommitID)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
// FIXME: graceful: These commands should likely have a timeout
|
|
ctx, cancel := context.WithCancel(git.DefaultContext)
|
|
defer cancel()
|
|
var cmd *exec.Cmd
|
|
if (len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA) && commit.ParentCount() == 0 {
|
|
cmd = exec.CommandContext(ctx, git.GitExecutable, "show", afterCommitID)
|
|
} else {
|
|
actualBeforeCommitID := beforeCommitID
|
|
if len(actualBeforeCommitID) == 0 {
|
|
parentCommit, _ := commit.Parent(0)
|
|
actualBeforeCommitID = parentCommit.ID.String()
|
|
}
|
|
diffArgs := []string{"diff", "-M"}
|
|
if len(whitespaceBehavior) != 0 {
|
|
diffArgs = append(diffArgs, whitespaceBehavior)
|
|
}
|
|
diffArgs = append(diffArgs, actualBeforeCommitID)
|
|
diffArgs = append(diffArgs, afterCommitID)
|
|
cmd = exec.CommandContext(ctx, git.GitExecutable, diffArgs...)
|
|
beforeCommitID = actualBeforeCommitID
|
|
}
|
|
cmd.Dir = repoPath
|
|
cmd.Stderr = os.Stderr
|
|
|
|
stdout, err := cmd.StdoutPipe()
|
|
if err != nil {
|
|
return nil, fmt.Errorf("StdoutPipe: %v", err)
|
|
}
|
|
|
|
if err = cmd.Start(); err != nil {
|
|
return nil, fmt.Errorf("Start: %v", err)
|
|
}
|
|
|
|
pid := process.GetManager().Add(fmt.Sprintf("GetDiffRange [repo_path: %s]", repoPath), cancel)
|
|
defer process.GetManager().Remove(pid)
|
|
|
|
diff, err := ParsePatch(maxLines, maxLineCharacters, maxFiles, stdout)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("ParsePatch: %v", err)
|
|
}
|
|
for _, diffFile := range diff.Files {
|
|
tailSection := diffFile.GetTailSection(gitRepo, beforeCommitID, afterCommitID)
|
|
if tailSection != nil {
|
|
diffFile.Sections = append(diffFile.Sections, tailSection)
|
|
}
|
|
}
|
|
|
|
if err = cmd.Wait(); err != nil {
|
|
return nil, fmt.Errorf("Wait: %v", err)
|
|
}
|
|
|
|
shortstatArgs := []string{beforeCommitID + "..." + afterCommitID}
|
|
if len(beforeCommitID) == 0 || beforeCommitID == git.EmptySHA {
|
|
shortstatArgs = []string{git.EmptyTreeSHA, afterCommitID}
|
|
}
|
|
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
|
|
if err != nil && strings.Contains(err.Error(), "no merge base") {
|
|
// git >= 2.28 now returns an error if base and head have become unrelated.
|
|
// previously it would return the results of git diff --shortstat base head so let's try that...
|
|
shortstatArgs = []string{beforeCommitID, afterCommitID}
|
|
diff.NumFiles, diff.TotalAddition, diff.TotalDeletion, err = git.GetDiffShortStat(repoPath, shortstatArgs...)
|
|
}
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
return diff, nil
|
|
}
|
|
|
|
// GetDiffCommit builds a Diff representing the given commitID.
|
|
func GetDiffCommit(repoPath, commitID string, maxLines, maxLineCharacters, maxFiles int) (*Diff, error) {
|
|
return GetDiffRange(repoPath, "", commitID, maxLines, maxLineCharacters, maxFiles)
|
|
}
|
|
|
|
// CommentAsDiff returns c.Patch as *Diff
|
|
func CommentAsDiff(c *models.Comment) (*Diff, error) {
|
|
diff, err := ParsePatch(setting.Git.MaxGitDiffLines,
|
|
setting.Git.MaxGitDiffLineCharacters, setting.Git.MaxGitDiffFiles, strings.NewReader(c.Patch))
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(diff.Files) == 0 {
|
|
return nil, fmt.Errorf("no file found for comment ID: %d", c.ID)
|
|
}
|
|
secs := diff.Files[0].Sections
|
|
if len(secs) == 0 {
|
|
return nil, fmt.Errorf("no sections found for comment ID: %d", c.ID)
|
|
}
|
|
return diff, nil
|
|
}
|
|
|
|
// CommentMustAsDiff executes AsDiff and logs the error instead of returning
|
|
func CommentMustAsDiff(c *models.Comment) *Diff {
|
|
diff, err := CommentAsDiff(c)
|
|
if err != nil {
|
|
log.Warn("CommentMustAsDiff: %v", err)
|
|
}
|
|
return diff
|
|
}
|