1436 lines · 44.1 KB · Go
RawDownload
// Copyright 2014 The Gogs Authors. All rights reserved.// Copyright 2019 The Gitea Authors. All rights reserved.// SPDX-License-Identifier: MIT package gitdiff import (	"bufio"	"bytes"	"cmp"	"context"	"fmt"	"html"	"html/template"	"io"	"net/url"	"strings"	"time" 	"forgejo.org/models/db"	git_model "forgejo.org/models/git"	issues_model "forgejo.org/models/issues"	pull_model "forgejo.org/models/pull"	user_model "forgejo.org/models/user"	"forgejo.org/modules/analyze"	"forgejo.org/modules/charset"	"forgejo.org/modules/git"	"forgejo.org/modules/highlight"	"forgejo.org/modules/lfs"	"forgejo.org/modules/log"	"forgejo.org/modules/setting"	"forgejo.org/modules/translation" 	"github.com/sergi/go-diff/diffmatchpatch"	stdcharset "golang.org/x/net/html/charset"	"golang.org/x/text/encoding"	"golang.org/x/text/transform") // DiffLineType represents the type of DiffLine.type DiffLineType uint8 // DiffLineType possible values.const (	DiffLinePlain DiffLineType = iota + 1	DiffLineAdd	DiffLineDel	DiffLineSection) // DiffFileType represents the type of DiffFile.type DiffFileType uint8 // DiffFileType possible values.const (	DiffFileAdd DiffFileType = iota + 1	DiffFileChange	DiffFileDel	DiffFileRename	DiffFileCopy) // 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	Match         int	Type          DiffLineType	Content       string	Conversations []issues_model.CodeConversation	SectionInfo   *DiffLineSectionInfo} // DiffLineSectionInfo represents diff line section meta datatype DiffLineSectionInfo struct {	Path string 	// Last(Left/Right)Idx do not directly relate to this diff section, but indicate the last line number in the	// previous diff section. Set to 0 for the first diff section of a file, and 1 for the first line of code in the	// file.	LastLeftIdx  int	LastRightIdx int 	// (Left/Right)Idx are the first line number in this diff section	LeftIdx  int	RightIdx int 	// Number of lines contained within each diff section.  In the UI, these fields are set to 0 in cases where a	// section is being used as a placeholder at the end of a diff to allow expansion into the remainder of the file.	LeftHunkSize  int	RightHunkSize int} // BlobExcerptChunkSize represent max lines of excerptconst BlobExcerptChunkSize = 20 // GetType returns the type of DiffLine.func (d *DiffLine) GetType() int {	return int(d.Type)} // GetHTMLDiffLineType returns the diff line type name for HTMLfunc (d *DiffLine) GetHTMLDiffLineType() string {	switch d.Type {	case DiffLineAdd:		return "add"	case DiffLineDel:		return "del"	case DiffLineSection:		return "tag"	}	return "same"} // CanComment returns whether a line can get commentedfunc (d *DiffLine) CanComment() bool {	return len(d.Conversations) == 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.Conversations) == 0 || len(d.Conversations[0]) == 0 {		return ""	}	return d.Conversations[0][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.LeftIdx-d.SectionInfo.LastLeftIdx <= 1 || 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-1 > 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 purposesfunc getLineContent(content string, locale translation.Locale) DiffInline {	if len(content) > 0 {		return DiffInlineWithUnicodeEscape(template.HTML(html.EscapeString(content)), locale)	}	return DiffInline{EscapeStatus: &charset.EscapeStatus{}, Content: "<br>"}} // DiffSection represents a section of a DiffFile.type DiffSection struct {	file     *DiffFile	FileName string	Name     string	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 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} // DiffInline is a struct that has a content and escape statustype DiffInline struct {	EscapeStatus *charset.EscapeStatus	Content      template.HTML} // DiffInlineWithUnicodeEscape makes a DiffInline with hidden unicode characters escapedfunc DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {	status, content := charset.EscapeControlHTML(s, locale, charset.DiffContext)	return DiffInline{EscapeStatus: status, Content: content}} // DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escapedfunc DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {	highlighted, _ := highlight.Code(fileName, language, code)	status, content := charset.EscapeControlHTML(highlighted, locale, charset.DiffContext)	return DiffInline{EscapeStatus: status, Content: content}} // 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	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)	} 	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	IsSubmodule               bool	Sections                  []*DiffSection	IsIncomplete              bool	IsIncompleteLineTooLong   bool	IsProtected               bool	IsGenerated               bool	IsVendored                bool	IsViewed                  bool // User specific	HasChangedSinceLastReview bool // User specific	Language                  string	Mode                      string	OldMode                   string} // 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 filefunc (diffFile *DiffFile) GetTailSection(gitRepo *git.Repository, leftCommit, rightCommit *git.Commit) *DiffSection {	if len(diffFile.Sections) == 0 || diffFile.Type != DiffFileChange || diffFile.IsBin || diffFile.IsLFSFile {		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} // GetDiffFileName returns the name of the diff file, or its old name in case it was deletedfunc (diffFile *DiffFile) GetDiffFileName() string {	if diffFile.Name == "" {		return diffFile.OldName	}	return diffFile.Name} func (diffFile *DiffFile) ShouldBeHidden() bool {	return diffFile.IsGenerated || diffFile.IsViewed} func (diffFile *DiffFile) ModeTranslationKey(mode string) string {	switch mode {	case "040000":		return "git.filemode.directory"	case "100644":		return "git.filemode.normal_file"	case "100755":		return "git.filemode.executable_file"	case "120000":		return "git.filemode.symbolic_link"	case "160000":		return "git.filemode.submodule"	default:		return mode	}} func getCommitFileLineCount(commit *git.Commit, filePath string) int {	blob, err := commit.GetBlobByPath(filePath)	if err != nil {		return 0	}	reader, err := blob.DataAsync()	if err != nil {		return 0	}	defer reader.Close()	buf := make([]byte, 32*1024)	count := 1	lineSep := []byte{'\n'} 	c, err := reader.Read(buf)	if c == 0 && err == io.EOF {		return 0	}	for {		count += bytes.Count(buf[:c], lineSep)		switch {		case err == io.EOF:			return count		case err != nil:			return count		}		c, err = reader.Read(buf)	}} // Diff represents a difference between two git trees.type Diff struct {	Start, End                   string	NumFiles                     int	TotalAddition, TotalDeletion int	Files                        []*DiffFile	IsIncomplete                 bool	NumViewedFiles               int // user-specific} // LoadComments loads comments into each linefunc (diff *Diff) LoadComments(ctx context.Context, issue *issues_model.Issue, currentUser *user_model.User, showOutdatedComments bool) error {	allConversations, err := issues_model.FetchCodeConversations(ctx, issue, currentUser, showOutdatedComments)	if err != nil {		return err	}	for _, file := range diff.Files {		if lineCommits, ok := allConversations[file.Name]; ok {			for _, section := range file.Sections {				for _, line := range section.Lines {					if conversations, ok := lineCommits[int64(line.LeftIdx*-1)]; ok {						line.Conversations = append(line.Conversations, conversations...)					}					if comments, ok := lineCommits[int64(line.RightIdx)]; ok {

Lines 1–500 of 1436.