mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-11-03 20:36:07 +01:00 
			
		
		
		
	Full-file syntax highlighting for diff pages (#33766)
Fix #33358, fix #21970 This adds a step in the `GitDiffForRender` that does syntax highlighting for the entire file and then only references lines from that syntax highlighted code. This allows things like multi-line comments to be syntax highlighted correctly. --------- Co-authored-by: wxiaoguang <wxiaoguang@gmail.com>
This commit is contained in:
		@@ -31,6 +31,7 @@ import (
 | 
			
		||||
	"code.gitea.io/gitea/modules/optional"
 | 
			
		||||
	"code.gitea.io/gitea/modules/setting"
 | 
			
		||||
	"code.gitea.io/gitea/modules/translation"
 | 
			
		||||
	"code.gitea.io/gitea/modules/util"
 | 
			
		||||
 | 
			
		||||
	"github.com/sergi/go-diff/diffmatchpatch"
 | 
			
		||||
	stdcharset "golang.org/x/net/html/charset"
 | 
			
		||||
@@ -75,12 +76,12 @@ const (
 | 
			
		||||
 | 
			
		||||
// DiffLine represents a line difference in a DiffSection.
 | 
			
		||||
type DiffLine struct {
 | 
			
		||||
	LeftIdx     int
 | 
			
		||||
	RightIdx    int
 | 
			
		||||
	Match       int
 | 
			
		||||
	LeftIdx     int // line number, 1-based
 | 
			
		||||
	RightIdx    int // line number, 1-based
 | 
			
		||||
	Match       int // line number, 1-based
 | 
			
		||||
	Type        DiffLineType
 | 
			
		||||
	Content     string
 | 
			
		||||
	Comments    issues_model.CommentList
 | 
			
		||||
	Comments    issues_model.CommentList // related PR code comments
 | 
			
		||||
	SectionInfo *DiffLineSectionInfo
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
@@ -95,9 +96,18 @@ type DiffLineSectionInfo struct {
 | 
			
		||||
	RightHunkSize int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffHTMLOperation is the HTML version of diffmatchpatch.Diff
 | 
			
		||||
type DiffHTMLOperation struct {
 | 
			
		||||
	Type diffmatchpatch.Operation
 | 
			
		||||
	HTML template.HTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// BlobExcerptChunkSize represent max lines of excerpt
 | 
			
		||||
const BlobExcerptChunkSize = 20
 | 
			
		||||
 | 
			
		||||
// MaxDiffHighlightEntireFileSize is the maximum file size that will be highlighted with "entire file diff"
 | 
			
		||||
const MaxDiffHighlightEntireFileSize = 1 * 1024 * 1024
 | 
			
		||||
 | 
			
		||||
// GetType returns the type of DiffLine.
 | 
			
		||||
func (d *DiffLine) GetType() int {
 | 
			
		||||
	return int(d.Type)
 | 
			
		||||
@@ -112,8 +122,9 @@ func (d *DiffLine) GetHTMLDiffLineType() string {
 | 
			
		||||
		return "del"
 | 
			
		||||
	case DiffLineSection:
 | 
			
		||||
		return "tag"
 | 
			
		||||
	default:
 | 
			
		||||
		return "same"
 | 
			
		||||
	}
 | 
			
		||||
	return "same"
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// CanComment returns whether a line can get commented
 | 
			
		||||
@@ -196,38 +207,6 @@ type DiffSection struct {
 | 
			
		||||
	Lines    []*DiffLine
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var (
 | 
			
		||||
	addedCodePrefix   = []byte(`<span class="added-code">`)
 | 
			
		||||
	removedCodePrefix = []byte(`<span class="removed-code">`)
 | 
			
		||||
	codeTagSuffix     = []byte(`</span>`)
 | 
			
		||||
)
 | 
			
		||||
 | 
			
		||||
func diffToHTML(lineWrapperTags []string, diffs []diffmatchpatch.Diff, lineType DiffLineType) string {
 | 
			
		||||
	buf := bytes.NewBuffer(nil)
 | 
			
		||||
	// restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary
 | 
			
		||||
	for _, tag := range lineWrapperTags {
 | 
			
		||||
		buf.WriteString(tag)
 | 
			
		||||
	}
 | 
			
		||||
	for _, diff := range diffs {
 | 
			
		||||
		switch {
 | 
			
		||||
		case diff.Type == diffmatchpatch.DiffEqual:
 | 
			
		||||
			buf.WriteString(diff.Text)
 | 
			
		||||
		case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd:
 | 
			
		||||
			buf.Write(addedCodePrefix)
 | 
			
		||||
			buf.WriteString(diff.Text)
 | 
			
		||||
			buf.Write(codeTagSuffix)
 | 
			
		||||
		case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel:
 | 
			
		||||
			buf.Write(removedCodePrefix)
 | 
			
		||||
			buf.WriteString(diff.Text)
 | 
			
		||||
			buf.Write(codeTagSuffix)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	for range lineWrapperTags {
 | 
			
		||||
		buf.WriteString("</span>")
 | 
			
		||||
	}
 | 
			
		||||
	return buf.String()
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetLine gets a specific line by type (add or del) and file line number
 | 
			
		||||
func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
 | 
			
		||||
	var (
 | 
			
		||||
@@ -271,10 +250,10 @@ LOOP:
 | 
			
		||||
	return nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
var diffMatchPatch = diffmatchpatch.New()
 | 
			
		||||
 | 
			
		||||
func init() {
 | 
			
		||||
	diffMatchPatch.DiffEditCost = 100
 | 
			
		||||
func defaultDiffMatchPatch() *diffmatchpatch.DiffMatchPatch {
 | 
			
		||||
	dmp := diffmatchpatch.New()
 | 
			
		||||
	dmp.DiffEditCost = 100
 | 
			
		||||
	return dmp
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffInline is a struct that has a content and escape status
 | 
			
		||||
@@ -283,97 +262,114 @@ type DiffInline struct {
 | 
			
		||||
	Content      template.HTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escaped
 | 
			
		||||
// DiffInlineWithUnicodeEscape makes a DiffInline with hidden Unicode characters escaped
 | 
			
		||||
func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {
 | 
			
		||||
	status, content := charset.EscapeControlHTML(s, locale)
 | 
			
		||||
	return DiffInline{EscapeStatus: status, Content: content}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
 | 
			
		||||
func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
 | 
			
		||||
	highlighted, _ := highlight.Code(fileName, language, code)
 | 
			
		||||
	status, content := charset.EscapeControlHTML(highlighted, locale)
 | 
			
		||||
	return DiffInline{EscapeStatus: status, Content: content}
 | 
			
		||||
func (diffSection *DiffSection) getLineContentForRender(lineIdx int, diffLine *DiffLine, fileLanguage string, highlightLines map[int]template.HTML) template.HTML {
 | 
			
		||||
	h, ok := highlightLines[lineIdx-1]
 | 
			
		||||
	if ok {
 | 
			
		||||
		return h
 | 
			
		||||
	}
 | 
			
		||||
	if diffLine.Content == "" {
 | 
			
		||||
		return ""
 | 
			
		||||
	}
 | 
			
		||||
	if setting.Git.DisableDiffHighlight {
 | 
			
		||||
		return template.HTML(html.EscapeString(diffLine.Content[1:]))
 | 
			
		||||
	}
 | 
			
		||||
	h, _ = highlight.Code(diffSection.Name, fileLanguage, diffLine.Content[1:])
 | 
			
		||||
	return h
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (diffSection *DiffSection) getDiffLineForRender(diffLineType DiffLineType, leftLine, rightLine *DiffLine, locale translation.Locale) DiffInline {
 | 
			
		||||
	var fileLanguage string
 | 
			
		||||
	var highlightedLeftLines, highlightedRightLines map[int]template.HTML
 | 
			
		||||
	// when a "diff section" is manually prepared by ExcerptBlob, it doesn't have "file" information
 | 
			
		||||
	if diffSection.file != nil {
 | 
			
		||||
		fileLanguage = diffSection.file.Language
 | 
			
		||||
		highlightedLeftLines, highlightedRightLines = diffSection.file.highlightedLeftLines, diffSection.file.highlightedRightLines
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hcd := newHighlightCodeDiff()
 | 
			
		||||
	var diff1, diff2, lineHTML template.HTML
 | 
			
		||||
	if leftLine != nil {
 | 
			
		||||
		diff1 = diffSection.getLineContentForRender(leftLine.LeftIdx, leftLine, fileLanguage, highlightedLeftLines)
 | 
			
		||||
		lineHTML = util.Iif(diffLineType == DiffLinePlain, diff1, "")
 | 
			
		||||
	}
 | 
			
		||||
	if rightLine != nil {
 | 
			
		||||
		diff2 = diffSection.getLineContentForRender(rightLine.RightIdx, rightLine, fileLanguage, highlightedRightLines)
 | 
			
		||||
		lineHTML = util.Iif(diffLineType == DiffLinePlain, diff2, "")
 | 
			
		||||
	}
 | 
			
		||||
	if diffLineType != DiffLinePlain {
 | 
			
		||||
		// it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back
 | 
			
		||||
		// if the line wrappers are still needed in the future, it can be added back by "diffLineWithHighlightWrapper(hcd.lineWrapperTags. ...)"
 | 
			
		||||
		lineHTML = hcd.diffLineWithHighlight(diffLineType, diff1, diff2)
 | 
			
		||||
	}
 | 
			
		||||
	return DiffInlineWithUnicodeEscape(lineHTML, locale)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetComputedInlineDiffFor computes inline diff for the given line.
 | 
			
		||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline {
 | 
			
		||||
	if setting.Git.DisableDiffHighlight {
 | 
			
		||||
		return getLineContent(diffLine.Content[1:], locale)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	var (
 | 
			
		||||
		compareDiffLine *DiffLine
 | 
			
		||||
		diff1           string
 | 
			
		||||
		diff2           string
 | 
			
		||||
	)
 | 
			
		||||
 | 
			
		||||
	language := ""
 | 
			
		||||
	if diffSection.file != nil {
 | 
			
		||||
		language = diffSection.file.Language
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// try to find equivalent diff line. ignore, otherwise
 | 
			
		||||
	switch diffLine.Type {
 | 
			
		||||
	case DiffLineSection:
 | 
			
		||||
		return getLineContent(diffLine.Content[1:], locale)
 | 
			
		||||
	case DiffLineAdd:
 | 
			
		||||
		compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
 | 
			
		||||
		if compareDiffLine == nil {
 | 
			
		||||
			return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
 | 
			
		||||
		}
 | 
			
		||||
		diff1 = compareDiffLine.Content
 | 
			
		||||
		diff2 = diffLine.Content
 | 
			
		||||
		compareDiffLine := diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
 | 
			
		||||
		return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale)
 | 
			
		||||
	case DiffLineDel:
 | 
			
		||||
		compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
 | 
			
		||||
		if compareDiffLine == nil {
 | 
			
		||||
			return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
 | 
			
		||||
		}
 | 
			
		||||
		diff1 = diffLine.Content
 | 
			
		||||
		diff2 = compareDiffLine.Content
 | 
			
		||||
	default:
 | 
			
		||||
		if strings.IndexByte(" +-", diffLine.Content[0]) > -1 {
 | 
			
		||||
			return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
 | 
			
		||||
		}
 | 
			
		||||
		return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content, locale)
 | 
			
		||||
		compareDiffLine := diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
 | 
			
		||||
		return diffSection.getDiffLineForRender(DiffLineDel, diffLine, compareDiffLine, locale)
 | 
			
		||||
	default: // Plain
 | 
			
		||||
		// TODO: there was an "if" check: `if diffLine.Content >strings.IndexByte(" +-", diffLine.Content[0]) > -1 { ... } else { ... }`
 | 
			
		||||
		// no idea why it needs that check, it seems that the "if" should be always true, so try to simplify the code
 | 
			
		||||
		return diffSection.getDiffLineForRender(DiffLinePlain, nil, diffLine, locale)
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	hcd := newHighlightCodeDiff()
 | 
			
		||||
	diffRecord := hcd.diffWithHighlight(diffSection.FileName, language, diff1[1:], diff2[1:])
 | 
			
		||||
	// it seems that Gitea doesn't need the line wrapper of Chroma, so do not add them back
 | 
			
		||||
	// if the line wrappers are still needed in the future, it can be added back by "diffToHTML(hcd.lineWrapperTags. ...)"
 | 
			
		||||
	diffHTML := diffToHTML(nil, diffRecord, diffLine.Type)
 | 
			
		||||
	return DiffInlineWithUnicodeEscape(template.HTML(diffHTML), locale)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// DiffFile represents a file diff.
 | 
			
		||||
type DiffFile struct {
 | 
			
		||||
	Name                      string
 | 
			
		||||
	NameHash                  string
 | 
			
		||||
	OldName                   string
 | 
			
		||||
	Index                     int
 | 
			
		||||
	Addition, Deletion        int
 | 
			
		||||
	Type                      DiffFileType
 | 
			
		||||
	IsCreated                 bool
 | 
			
		||||
	IsDeleted                 bool
 | 
			
		||||
	IsBin                     bool
 | 
			
		||||
	IsLFSFile                 bool
 | 
			
		||||
	IsRenamed                 bool
 | 
			
		||||
	IsAmbiguous               bool
 | 
			
		||||
	Sections                  []*DiffSection
 | 
			
		||||
	IsIncomplete              bool
 | 
			
		||||
	IsIncompleteLineTooLong   bool
 | 
			
		||||
	IsProtected               bool
 | 
			
		||||
	IsGenerated               bool
 | 
			
		||||
	IsVendored                bool
 | 
			
		||||
	// only used internally to parse Ambiguous filenames
 | 
			
		||||
	isAmbiguous bool
 | 
			
		||||
 | 
			
		||||
	// basic fields (parsed from diff result)
 | 
			
		||||
	Name        string
 | 
			
		||||
	NameHash    string
 | 
			
		||||
	OldName     string
 | 
			
		||||
	Addition    int
 | 
			
		||||
	Deletion    int
 | 
			
		||||
	Type        DiffFileType
 | 
			
		||||
	Mode        string
 | 
			
		||||
	OldMode     string
 | 
			
		||||
	IsCreated   bool
 | 
			
		||||
	IsDeleted   bool
 | 
			
		||||
	IsBin       bool
 | 
			
		||||
	IsLFSFile   bool
 | 
			
		||||
	IsRenamed   bool
 | 
			
		||||
	IsSubmodule bool
 | 
			
		||||
	// basic fields but for render purpose only
 | 
			
		||||
	Sections                []*DiffSection
 | 
			
		||||
	IsIncomplete            bool
 | 
			
		||||
	IsIncompleteLineTooLong bool
 | 
			
		||||
 | 
			
		||||
	// will be filled by the extra loop in GitDiffForRender
 | 
			
		||||
	Language          string
 | 
			
		||||
	IsGenerated       bool
 | 
			
		||||
	IsVendored        bool
 | 
			
		||||
	SubmoduleDiffInfo *SubmoduleDiffInfo // IsSubmodule==true, then there must be a SubmoduleDiffInfo
 | 
			
		||||
 | 
			
		||||
	// will be filled by route handler
 | 
			
		||||
	IsProtected bool
 | 
			
		||||
 | 
			
		||||
	// will be filled by SyncUserSpecificDiff
 | 
			
		||||
	IsViewed                  bool // User specific
 | 
			
		||||
	HasChangedSinceLastReview bool // User specific
 | 
			
		||||
	Language                  string
 | 
			
		||||
	Mode                      string
 | 
			
		||||
	OldMode                   string
 | 
			
		||||
 | 
			
		||||
	IsSubmodule       bool // if IsSubmodule==true, then there must be a SubmoduleDiffInfo
 | 
			
		||||
	SubmoduleDiffInfo *SubmoduleDiffInfo
 | 
			
		||||
	// for render purpose only, will be filled by the extra loop in GitDiffForRender
 | 
			
		||||
	highlightedLeftLines  map[int]template.HTML
 | 
			
		||||
	highlightedRightLines map[int]template.HTML
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetType returns type of diff file.
 | 
			
		||||
@@ -381,18 +377,23 @@ 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(leftCommit, rightCommit *git.Commit) *DiffSection {
 | 
			
		||||
type DiffLimitedContent struct {
 | 
			
		||||
	LeftContent, RightContent *limitByteWriter
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetTailSectionAndLimitedContent creates a fake DiffLineSection if the last section is not the end of the file
 | 
			
		||||
func (diffFile *DiffFile) GetTailSectionAndLimitedContent(leftCommit, rightCommit *git.Commit) (_ *DiffSection, diffLimitedContent DiffLimitedContent) {
 | 
			
		||||
	if len(diffFile.Sections) == 0 || leftCommit == nil || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, diffLimitedContent
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	lastSection := diffFile.Sections[len(diffFile.Sections)-1]
 | 
			
		||||
	lastLine := lastSection.Lines[len(lastSection.Lines)-1]
 | 
			
		||||
	leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name)
 | 
			
		||||
	rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name)
 | 
			
		||||
	leftLineCount, leftContent := getCommitFileLineCountAndLimitedContent(leftCommit, diffFile.Name)
 | 
			
		||||
	rightLineCount, rightContent := getCommitFileLineCountAndLimitedContent(rightCommit, diffFile.Name)
 | 
			
		||||
	diffLimitedContent = DiffLimitedContent{LeftContent: leftContent, RightContent: rightContent}
 | 
			
		||||
	if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
 | 
			
		||||
		return nil
 | 
			
		||||
		return nil, diffLimitedContent
 | 
			
		||||
	}
 | 
			
		||||
	tailDiffLine := &DiffLine{
 | 
			
		||||
		Type:    DiffLineSection,
 | 
			
		||||
@@ -406,7 +407,7 @@ func (diffFile *DiffFile) GetTailSection(leftCommit, rightCommit *git.Commit) *D
 | 
			
		||||
		},
 | 
			
		||||
	}
 | 
			
		||||
	tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}}
 | 
			
		||||
	return tailSection
 | 
			
		||||
	return tailSection, diffLimitedContent
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDiffFileName returns the name of the diff file, or its old name in case it was deleted
 | 
			
		||||
@@ -438,16 +439,29 @@ func (diffFile *DiffFile) ModeTranslationKey(mode string) string {
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCommitFileLineCount(commit *git.Commit, filePath string) int {
 | 
			
		||||
type limitByteWriter struct {
 | 
			
		||||
	buf   bytes.Buffer
 | 
			
		||||
	limit int
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func (l *limitByteWriter) Write(p []byte) (n int, err error) {
 | 
			
		||||
	if l.buf.Len()+len(p) > l.limit {
 | 
			
		||||
		p = p[:l.limit-l.buf.Len()]
 | 
			
		||||
	}
 | 
			
		||||
	return l.buf.Write(p)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func getCommitFileLineCountAndLimitedContent(commit *git.Commit, filePath string) (lineCount int, limitWriter *limitByteWriter) {
 | 
			
		||||
	blob, err := commit.GetBlobByPath(filePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, nil
 | 
			
		||||
	}
 | 
			
		||||
	lineCount, err := blob.GetBlobLineCount()
 | 
			
		||||
	w := &limitByteWriter{limit: MaxDiffHighlightEntireFileSize + 1}
 | 
			
		||||
	lineCount, err = blob.GetBlobLineCount(w)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return 0
 | 
			
		||||
		return 0, nil
 | 
			
		||||
	}
 | 
			
		||||
	return lineCount
 | 
			
		||||
	return lineCount, w
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// Diff represents a difference between two git trees.
 | 
			
		||||
@@ -526,13 +540,13 @@ parsingLoop:
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if maxFiles > -1 && len(diff.Files) >= maxFiles {
 | 
			
		||||
			lastFile := createDiffFile(diff, line)
 | 
			
		||||
			lastFile := createDiffFile(line)
 | 
			
		||||
			diff.End = lastFile.Name
 | 
			
		||||
			diff.IsIncomplete = true
 | 
			
		||||
			break parsingLoop
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		curFile = createDiffFile(diff, line)
 | 
			
		||||
		curFile = createDiffFile(line)
 | 
			
		||||
		if skipping {
 | 
			
		||||
			if curFile.Name != skipToFile {
 | 
			
		||||
				line, err = skipToNextDiffHead(input)
 | 
			
		||||
@@ -615,28 +629,28 @@ parsingLoop:
 | 
			
		||||
			case strings.HasPrefix(line, "rename from "):
 | 
			
		||||
				curFile.IsRenamed = true
 | 
			
		||||
				curFile.Type = DiffFileRename
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					curFile.OldName = prepareValue(line, "rename from ")
 | 
			
		||||
				}
 | 
			
		||||
			case strings.HasPrefix(line, "rename to "):
 | 
			
		||||
				curFile.IsRenamed = true
 | 
			
		||||
				curFile.Type = DiffFileRename
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					curFile.Name = prepareValue(line, "rename to ")
 | 
			
		||||
					curFile.IsAmbiguous = false
 | 
			
		||||
					curFile.isAmbiguous = false
 | 
			
		||||
				}
 | 
			
		||||
			case strings.HasPrefix(line, "copy from "):
 | 
			
		||||
				curFile.IsRenamed = true
 | 
			
		||||
				curFile.Type = DiffFileCopy
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					curFile.OldName = prepareValue(line, "copy from ")
 | 
			
		||||
				}
 | 
			
		||||
			case strings.HasPrefix(line, "copy to "):
 | 
			
		||||
				curFile.IsRenamed = true
 | 
			
		||||
				curFile.Type = DiffFileCopy
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					curFile.Name = prepareValue(line, "copy to ")
 | 
			
		||||
					curFile.IsAmbiguous = false
 | 
			
		||||
					curFile.isAmbiguous = false
 | 
			
		||||
				}
 | 
			
		||||
			case strings.HasPrefix(line, "new file"):
 | 
			
		||||
				curFile.Type = DiffFileAdd
 | 
			
		||||
@@ -663,7 +677,7 @@ parsingLoop:
 | 
			
		||||
				curFile.IsBin = true
 | 
			
		||||
			case strings.HasPrefix(line, "--- "):
 | 
			
		||||
				// Handle ambiguous filenames
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					// The shortest string that can end up here is:
 | 
			
		||||
					// "--- a\t\n" without the quotes.
 | 
			
		||||
					// This line has a len() of 7 but doesn't contain a oldName.
 | 
			
		||||
@@ -681,7 +695,7 @@ parsingLoop:
 | 
			
		||||
				// Otherwise do nothing with this line
 | 
			
		||||
			case strings.HasPrefix(line, "+++ "):
 | 
			
		||||
				// Handle ambiguous filenames
 | 
			
		||||
				if curFile.IsAmbiguous {
 | 
			
		||||
				if curFile.isAmbiguous {
 | 
			
		||||
					if len(line) > 6 && line[4] == 'b' {
 | 
			
		||||
						curFile.Name = line[6 : len(line)-1]
 | 
			
		||||
						if line[len(line)-2] == '\t' {
 | 
			
		||||
@@ -693,7 +707,7 @@ parsingLoop:
 | 
			
		||||
					} else {
 | 
			
		||||
						curFile.Name = curFile.OldName
 | 
			
		||||
					}
 | 
			
		||||
					curFile.IsAmbiguous = false
 | 
			
		||||
					curFile.isAmbiguous = false
 | 
			
		||||
				}
 | 
			
		||||
				// Otherwise do nothing with this line, but now switch to parsing hunks
 | 
			
		||||
				lineBytes, isFragment, err := parseHunks(ctx, curFile, maxLines, maxLineCharacters, input)
 | 
			
		||||
@@ -1006,7 +1020,7 @@ func parseHunks(ctx context.Context, curFile *DiffFile, maxLines, maxLineCharact
 | 
			
		||||
	}
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func createDiffFile(diff *Diff, line string) *DiffFile {
 | 
			
		||||
func createDiffFile(line string) *DiffFile {
 | 
			
		||||
	// The a/ and b/ filenames are the same unless rename/copy is involved.
 | 
			
		||||
	// Especially, even for a creation or a deletion, /dev/null is not used
 | 
			
		||||
	// in place of the a/ or b/ filenames.
 | 
			
		||||
@@ -1017,12 +1031,11 @@ func createDiffFile(diff *Diff, line string) *DiffFile {
 | 
			
		||||
	//
 | 
			
		||||
	// Path names are quoted if necessary.
 | 
			
		||||
	//
 | 
			
		||||
	// This means that you should always be able to determine the file name even when there
 | 
			
		||||
	// This means that you should always be able to determine the file name even when
 | 
			
		||||
	// there is potential ambiguity...
 | 
			
		||||
	//
 | 
			
		||||
	// but we can be simpler with our heuristics by just forcing git to prefix things nicely
 | 
			
		||||
	curFile := &DiffFile{
 | 
			
		||||
		Index:    len(diff.Files) + 1,
 | 
			
		||||
		Type:     DiffFileChange,
 | 
			
		||||
		Sections: make([]*DiffSection, 0, 10),
 | 
			
		||||
	}
 | 
			
		||||
@@ -1034,7 +1047,7 @@ func createDiffFile(diff *Diff, line string) *DiffFile {
 | 
			
		||||
	curFile.OldName, oldNameAmbiguity = readFileName(rd)
 | 
			
		||||
	curFile.Name, newNameAmbiguity = readFileName(rd)
 | 
			
		||||
	if oldNameAmbiguity && newNameAmbiguity {
 | 
			
		||||
		curFile.IsAmbiguous = true
 | 
			
		||||
		curFile.isAmbiguous = true
 | 
			
		||||
		// OK we should bet that the oldName and the newName are the same if they can be made to be same
 | 
			
		||||
		// So we need to start again ...
 | 
			
		||||
		if (len(line)-len(cmdDiffHead)-1)%2 == 0 {
 | 
			
		||||
@@ -1121,20 +1134,21 @@ func guessBeforeCommitForDiff(gitRepo *git.Repository, beforeCommitID string, af
 | 
			
		||||
	return actualBeforeCommit, actualBeforeCommitID, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// GetDiff builds a Diff between two commits of a repository.
 | 
			
		||||
// getDiffBasic 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 GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 | 
			
		||||
// Returned beforeCommit could be nil if the afterCommit doesn't have parent commit
 | 
			
		||||
func getDiffBasic(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (_ *Diff, beforeCommit, afterCommit *git.Commit, err error) {
 | 
			
		||||
	repoPath := gitRepo.Path
 | 
			
		||||
 | 
			
		||||
	afterCommit, err := gitRepo.GetCommit(opts.AfterCommitID)
 | 
			
		||||
	afterCommit, err = gitRepo.GetCommit(opts.AfterCommitID)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	actualBeforeCommit, actualBeforeCommitID, err := guessBeforeCommitForDiff(gitRepo, opts.BeforeCommitID, afterCommit)
 | 
			
		||||
	beforeCommit, beforeCommitID, err := guessBeforeCommitForDiff(gitRepo, opts.BeforeCommitID, afterCommit)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, nil, nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmdDiff := git.NewCommand().
 | 
			
		||||
@@ -1150,7 +1164,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 | 
			
		||||
		parsePatchSkipToFile = ""
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	cmdDiff.AddDynamicArguments(actualBeforeCommitID.String(), opts.AfterCommitID)
 | 
			
		||||
	cmdDiff.AddDynamicArguments(beforeCommitID.String(), opts.AfterCommitID)
 | 
			
		||||
	cmdDiff.AddDashesAndList(files...)
 | 
			
		||||
 | 
			
		||||
	cmdCtx, cmdCancel := context.WithCancel(ctx)
 | 
			
		||||
@@ -1180,12 +1194,25 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 | 
			
		||||
	// Ensure the git process is killed if it didn't exit already
 | 
			
		||||
	cmdCancel()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, fmt.Errorf("unable to ParsePatch: %w", err)
 | 
			
		||||
		return nil, nil, nil, fmt.Errorf("unable to ParsePatch: %w", err)
 | 
			
		||||
	}
 | 
			
		||||
	diff.Start = opts.SkipTo
 | 
			
		||||
	return diff, beforeCommit, afterCommit, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
	checker, deferable := gitRepo.CheckAttributeReader(opts.AfterCommitID)
 | 
			
		||||
	defer deferable()
 | 
			
		||||
func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 | 
			
		||||
	diff, _, _, err := getDiffBasic(ctx, gitRepo, opts, files...)
 | 
			
		||||
	return diff, err
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func GetDiffForRender(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
 | 
			
		||||
	diff, beforeCommit, afterCommit, err := getDiffBasic(ctx, gitRepo, opts, files...)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	checker, deferrable := gitRepo.CheckAttributeReader(opts.AfterCommitID)
 | 
			
		||||
	defer deferrable()
 | 
			
		||||
 | 
			
		||||
	for _, diffFile := range diff.Files {
 | 
			
		||||
		isVendored := optional.None[bool]()
 | 
			
		||||
@@ -1205,7 +1232,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 | 
			
		||||
 | 
			
		||||
		// Populate Submodule URLs
 | 
			
		||||
		if diffFile.SubmoduleDiffInfo != nil {
 | 
			
		||||
			diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, actualBeforeCommit, afterCommit)
 | 
			
		||||
			diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !isVendored.Has() {
 | 
			
		||||
@@ -1217,15 +1244,46 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
 | 
			
		||||
			isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name))
 | 
			
		||||
		}
 | 
			
		||||
		diffFile.IsGenerated = isGenerated.Value()
 | 
			
		||||
 | 
			
		||||
		tailSection := diffFile.GetTailSection(actualBeforeCommit, afterCommit)
 | 
			
		||||
		tailSection, limitedContent := diffFile.GetTailSectionAndLimitedContent(beforeCommit, afterCommit)
 | 
			
		||||
		if tailSection != nil {
 | 
			
		||||
			diffFile.Sections = append(diffFile.Sections, tailSection)
 | 
			
		||||
		}
 | 
			
		||||
 | 
			
		||||
		if !setting.Git.DisableDiffHighlight {
 | 
			
		||||
			if limitedContent.LeftContent != nil && limitedContent.LeftContent.buf.Len() < MaxDiffHighlightEntireFileSize {
 | 
			
		||||
				diffFile.highlightedLeftLines = highlightCodeLines(diffFile, true /* left */, limitedContent.LeftContent.buf.String())
 | 
			
		||||
			}
 | 
			
		||||
			if limitedContent.RightContent != nil && limitedContent.RightContent.buf.Len() < MaxDiffHighlightEntireFileSize {
 | 
			
		||||
				diffFile.highlightedRightLines = highlightCodeLines(diffFile, false /* right */, limitedContent.RightContent.buf.String())
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	return diff, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func highlightCodeLines(diffFile *DiffFile, isLeft bool, content string) map[int]template.HTML {
 | 
			
		||||
	highlightedNewContent, _ := highlight.Code(diffFile.Name, diffFile.Language, content)
 | 
			
		||||
	splitLines := strings.Split(string(highlightedNewContent), "\n")
 | 
			
		||||
	lines := make(map[int]template.HTML, len(splitLines))
 | 
			
		||||
	// only save the highlighted lines we need, but not the whole file, to save memory
 | 
			
		||||
	for _, sec := range diffFile.Sections {
 | 
			
		||||
		for _, ln := range sec.Lines {
 | 
			
		||||
			lineIdx := ln.LeftIdx
 | 
			
		||||
			if !isLeft {
 | 
			
		||||
				lineIdx = ln.RightIdx
 | 
			
		||||
			}
 | 
			
		||||
			if lineIdx >= 1 {
 | 
			
		||||
				idx := lineIdx - 1
 | 
			
		||||
				if idx < len(splitLines) {
 | 
			
		||||
					lines[idx] = template.HTML(splitLines[idx])
 | 
			
		||||
				}
 | 
			
		||||
			}
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return lines
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
type DiffShortStat struct {
 | 
			
		||||
	NumFiles, TotalAddition, TotalDeletion int
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user