mirror of
				https://github.com/go-gitea/gitea.git
				synced 2025-10-29 10:57:44 +09: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:
		| @@ -7,6 +7,7 @@ package git | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"encoding/base64" | ||||
| 	"errors" | ||||
| 	"io" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/typesniffer" | ||||
| @@ -34,8 +35,9 @@ func (b *Blob) GetBlobContent(limit int64) (string, error) { | ||||
| 	return string(buf), err | ||||
| } | ||||
|  | ||||
| // GetBlobLineCount gets line count of the blob | ||||
| func (b *Blob) GetBlobLineCount() (int, error) { | ||||
| // GetBlobLineCount gets line count of the blob. | ||||
| // It will also try to write the content to w if it's not nil, then we could pre-fetch the content without reading it again. | ||||
| func (b *Blob) GetBlobLineCount(w io.Writer) (int, error) { | ||||
| 	reader, err := b.DataAsync() | ||||
| 	if err != nil { | ||||
| 		return 0, err | ||||
| @@ -44,20 +46,20 @@ func (b *Blob) GetBlobLineCount() (int, error) { | ||||
| 	buf := make([]byte, 32*1024) | ||||
| 	count := 1 | ||||
| 	lineSep := []byte{'\n'} | ||||
|  | ||||
| 	c, err := reader.Read(buf) | ||||
| 	if c == 0 && err == io.EOF { | ||||
| 		return 0, nil | ||||
| 	} | ||||
| 	for { | ||||
| 		c, err := reader.Read(buf) | ||||
| 		if w != nil { | ||||
| 			if _, err := w.Write(buf[:c]); err != nil { | ||||
| 				return count, err | ||||
| 			} | ||||
| 		} | ||||
| 		count += bytes.Count(buf[:c], lineSep) | ||||
| 		switch { | ||||
| 		case err == io.EOF: | ||||
| 		case errors.Is(err, io.EOF): | ||||
| 			return count, nil | ||||
| 		case err != nil: | ||||
| 			return count, err | ||||
| 		} | ||||
| 		c, err = reader.Read(buf) | ||||
| 	} | ||||
| } | ||||
|  | ||||
|   | ||||
| @@ -11,6 +11,7 @@ import ( | ||||
| 	gohtml "html" | ||||
| 	"html/template" | ||||
| 	"io" | ||||
| 	"path" | ||||
| 	"path/filepath" | ||||
| 	"strings" | ||||
| 	"sync" | ||||
| @@ -83,7 +84,7 @@ func Code(fileName, language, code string) (output template.HTML, lexerName stri | ||||
| 	} | ||||
|  | ||||
| 	if lexer == nil { | ||||
| 		if val, ok := highlightMapping[filepath.Ext(fileName)]; ok { | ||||
| 		if val, ok := highlightMapping[path.Ext(fileName)]; ok { | ||||
| 			// use mapped value to find lexer | ||||
| 			lexer = lexers.Get(val) | ||||
| 		} | ||||
|   | ||||
| @@ -1591,8 +1591,7 @@ func GetPullRequestFiles(ctx *context.APIContext) { | ||||
| 	maxLines := setting.Git.MaxGitDiffLines | ||||
|  | ||||
| 	// FIXME: If there are too many files in the repo, may cause some unpredictable issues. | ||||
| 	// FIXME: it doesn't need to call "GetDiff" to do various parsing and highlighting | ||||
| 	diff, err := gitdiff.GetDiff(ctx, baseGitRepo, | ||||
| 	diff, err := gitdiff.GetDiffForAPI(ctx, baseGitRepo, | ||||
| 		&gitdiff.DiffOptions{ | ||||
| 			BeforeCommitID:     startCommitID, | ||||
| 			AfterCommitID:      endCommitID, | ||||
|   | ||||
| @@ -97,7 +97,7 @@ func RefBlame(ctx *context.Context) { | ||||
| 		return | ||||
| 	} | ||||
|  | ||||
| 	ctx.Data["NumLines"], err = blob.GetBlobLineCount() | ||||
| 	ctx.Data["NumLines"], err = blob.GetBlobLineCount(nil) | ||||
| 	if err != nil { | ||||
| 		ctx.NotFound(err) | ||||
| 		return | ||||
|   | ||||
| @@ -314,7 +314,7 @@ func Diff(ctx *context.Context) { | ||||
| 		maxLines, maxFiles = -1, -1 | ||||
| 	} | ||||
|  | ||||
| 	diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{ | ||||
| 	diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{ | ||||
| 		AfterCommitID:      commitID, | ||||
| 		SkipTo:             ctx.FormString("skip-to"), | ||||
| 		MaxLines:           maxLines, | ||||
|   | ||||
| @@ -614,7 +614,7 @@ func PrepareCompareDiff( | ||||
|  | ||||
| 	fileOnly := ctx.FormBool("file-only") | ||||
|  | ||||
| 	diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo, | ||||
| 	diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo, | ||||
| 		&gitdiff.DiffOptions{ | ||||
| 			BeforeCommitID:     beforeCommitID, | ||||
| 			AfterCommitID:      headCommitID, | ||||
|   | ||||
| @@ -749,7 +749,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi | ||||
| 		diffOptions.BeforeCommitID = startCommitID | ||||
| 	} | ||||
|  | ||||
| 	diff, err := gitdiff.GetDiff(ctx, gitRepo, diffOptions, files...) | ||||
| 	diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...) | ||||
| 	if err != nil { | ||||
| 		ctx.ServerError("GetDiff", err) | ||||
| 		return | ||||
|   | ||||
| @@ -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 | ||||
| } | ||||
|   | ||||
| @@ -17,27 +17,10 @@ import ( | ||||
| 	"code.gitea.io/gitea/modules/json" | ||||
| 	"code.gitea.io/gitea/modules/setting" | ||||
|  | ||||
| 	dmp "github.com/sergi/go-diff/diffmatchpatch" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestDiffToHTML(t *testing.T) { | ||||
| 	assert.Equal(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML(nil, []dmp.Diff{ | ||||
| 		{Type: dmp.DiffEqual, Text: "foo "}, | ||||
| 		{Type: dmp.DiffInsert, Text: "bar"}, | ||||
| 		{Type: dmp.DiffDelete, Text: " baz"}, | ||||
| 		{Type: dmp.DiffEqual, Text: " biz"}, | ||||
| 	}, DiffLineAdd)) | ||||
|  | ||||
| 	assert.Equal(t, "foo <span class=\"removed-code\">bar</span> biz", diffToHTML(nil, []dmp.Diff{ | ||||
| 		{Type: dmp.DiffEqual, Text: "foo "}, | ||||
| 		{Type: dmp.DiffDelete, Text: "bar"}, | ||||
| 		{Type: dmp.DiffInsert, Text: " baz"}, | ||||
| 		{Type: dmp.DiffEqual, Text: " biz"}, | ||||
| 	}, DiffLineDel)) | ||||
| } | ||||
|  | ||||
| func TestParsePatch_skipTo(t *testing.T) { | ||||
| 	type testcase struct { | ||||
| 		name        string | ||||
| @@ -621,7 +604,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) { | ||||
|  | ||||
| 	defer gitRepo.Close() | ||||
| 	for _, behavior := range []git.TrustedCmdArgs{{"-w"}, {"--ignore-space-at-eol"}, {"-b"}, nil} { | ||||
| 		diffs, err := GetDiff(t.Context(), gitRepo, | ||||
| 		diffs, err := GetDiffForAPI(t.Context(), gitRepo, | ||||
| 			&DiffOptions{ | ||||
| 				AfterCommitID:      "d8e0bbb45f200e67d9a784ce55bd90821af45ebd", | ||||
| 				BeforeCommitID:     "72866af952e98d02a73003501836074b286a78f6", | ||||
|   | ||||
| @@ -4,10 +4,10 @@ | ||||
| package gitdiff | ||||
|  | ||||
| import ( | ||||
| 	"bytes" | ||||
| 	"html/template" | ||||
| 	"strings" | ||||
|  | ||||
| 	"code.gitea.io/gitea/modules/highlight" | ||||
|  | ||||
| 	"github.com/sergi/go-diff/diffmatchpatch" | ||||
| ) | ||||
|  | ||||
| @@ -77,7 +77,7 @@ func (hcd *highlightCodeDiff) isInPlaceholderRange(r rune) bool { | ||||
| 	return hcd.placeholderBegin <= r && r < hcd.placeholderBegin+rune(hcd.placeholderMaxCount) | ||||
| } | ||||
|  | ||||
| func (hcd *highlightCodeDiff) collectUsedRunes(code string) { | ||||
| func (hcd *highlightCodeDiff) collectUsedRunes(code template.HTML) { | ||||
| 	for _, r := range code { | ||||
| 		if hcd.isInPlaceholderRange(r) { | ||||
| 			// put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore. | ||||
| @@ -86,27 +86,76 @@ func (hcd *highlightCodeDiff) collectUsedRunes(code string) { | ||||
| 	} | ||||
| } | ||||
|  | ||||
| func (hcd *highlightCodeDiff) diffWithHighlight(filename, language, codeA, codeB string) []diffmatchpatch.Diff { | ||||
| func (hcd *highlightCodeDiff) diffLineWithHighlight(lineType DiffLineType, codeA, codeB template.HTML) template.HTML { | ||||
| 	return hcd.diffLineWithHighlightWrapper(nil, lineType, codeA, codeB) | ||||
| } | ||||
|  | ||||
| func (hcd *highlightCodeDiff) diffLineWithHighlightWrapper(lineWrapperTags []string, lineType DiffLineType, codeA, codeB template.HTML) template.HTML { | ||||
| 	hcd.collectUsedRunes(codeA) | ||||
| 	hcd.collectUsedRunes(codeB) | ||||
|  | ||||
| 	highlightCodeA, _ := highlight.Code(filename, language, codeA) | ||||
| 	highlightCodeB, _ := highlight.Code(filename, language, codeB) | ||||
| 	convertedCodeA := hcd.convertToPlaceholders(codeA) | ||||
| 	convertedCodeB := hcd.convertToPlaceholders(codeB) | ||||
|  | ||||
| 	convertedCodeA := hcd.convertToPlaceholders(string(highlightCodeA)) | ||||
| 	convertedCodeB := hcd.convertToPlaceholders(string(highlightCodeB)) | ||||
| 	dmp := defaultDiffMatchPatch() | ||||
| 	diffs := dmp.DiffMain(convertedCodeA, convertedCodeB, true) | ||||
| 	diffs = dmp.DiffCleanupEfficiency(diffs) | ||||
|  | ||||
| 	diffs := diffMatchPatch.DiffMain(convertedCodeA, convertedCodeB, true) | ||||
| 	diffs = diffMatchPatch.DiffCleanupEfficiency(diffs) | ||||
| 	buf := bytes.NewBuffer(nil) | ||||
|  | ||||
| 	for i := range diffs { | ||||
| 		hcd.recoverOneDiff(&diffs[i]) | ||||
| 	// restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary | ||||
| 	for _, tag := range lineWrapperTags { | ||||
| 		buf.WriteString(tag) | ||||
| 	} | ||||
| 	return diffs | ||||
|  | ||||
| 	addedCodePrefix := hcd.registerTokenAsPlaceholder(`<span class="added-code">`) | ||||
| 	removedCodePrefix := hcd.registerTokenAsPlaceholder(`<span class="removed-code">`) | ||||
| 	codeTagSuffix := hcd.registerTokenAsPlaceholder(`</span>`) | ||||
|  | ||||
| 	if codeTagSuffix != 0 { | ||||
| 		for _, diff := range diffs { | ||||
| 			switch { | ||||
| 			case diff.Type == diffmatchpatch.DiffEqual: | ||||
| 				buf.WriteString(diff.Text) | ||||
| 			case diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd: | ||||
| 				buf.WriteRune(addedCodePrefix) | ||||
| 				buf.WriteString(diff.Text) | ||||
| 				buf.WriteRune(codeTagSuffix) | ||||
| 			case diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel: | ||||
| 				buf.WriteRune(removedCodePrefix) | ||||
| 				buf.WriteString(diff.Text) | ||||
| 				buf.WriteRune(codeTagSuffix) | ||||
| 			} | ||||
| 		} | ||||
| 	} else { | ||||
| 		// placeholder map space is exhausted | ||||
| 		for _, diff := range diffs { | ||||
| 			take := diff.Type == diffmatchpatch.DiffEqual || (diff.Type == diffmatchpatch.DiffInsert && lineType == DiffLineAdd) || (diff.Type == diffmatchpatch.DiffDelete && lineType == DiffLineDel) | ||||
| 			if take { | ||||
| 				buf.WriteString(diff.Text) | ||||
| 			} | ||||
| 		} | ||||
| 	} | ||||
| 	for range lineWrapperTags { | ||||
| 		buf.WriteString("</span>") | ||||
| 	} | ||||
| 	return hcd.recoverOneDiff(buf.String()) | ||||
| } | ||||
|  | ||||
| func (hcd *highlightCodeDiff) registerTokenAsPlaceholder(token string) rune { | ||||
| 	placeholder, ok := hcd.tokenPlaceholderMap[token] | ||||
| 	if !ok { | ||||
| 		placeholder = hcd.nextPlaceholder() | ||||
| 		if placeholder != 0 { | ||||
| 			hcd.tokenPlaceholderMap[token] = placeholder | ||||
| 			hcd.placeholderTokenMap[placeholder] = token | ||||
| 		} | ||||
| 	} | ||||
| 	return placeholder | ||||
| } | ||||
|  | ||||
| // convertToPlaceholders totally depends on Chroma's valid HTML output and its structure, do not use these functions for other purposes. | ||||
| func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string { | ||||
| func (hcd *highlightCodeDiff) convertToPlaceholders(htmlContent template.HTML) string { | ||||
| 	var tagStack []string | ||||
| 	res := strings.Builder{} | ||||
|  | ||||
| @@ -115,6 +164,7 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string { | ||||
| 	var beforeToken, token string | ||||
| 	var valid bool | ||||
|  | ||||
| 	htmlCode := string(htmlContent) | ||||
| 	// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>" | ||||
| 	for { | ||||
| 		beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode) | ||||
| @@ -151,14 +201,7 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string { | ||||
| 		} // else: impossible | ||||
|  | ||||
| 		// remember the placeholder and token in the map | ||||
| 		placeholder, ok := hcd.tokenPlaceholderMap[tokenInMap] | ||||
| 		if !ok { | ||||
| 			placeholder = hcd.nextPlaceholder() | ||||
| 			if placeholder != 0 { | ||||
| 				hcd.tokenPlaceholderMap[tokenInMap] = placeholder | ||||
| 				hcd.placeholderTokenMap[placeholder] = tokenInMap | ||||
| 			} | ||||
| 		} | ||||
| 		placeholder := hcd.registerTokenAsPlaceholder(tokenInMap) | ||||
|  | ||||
| 		if placeholder != 0 { | ||||
| 			res.WriteRune(placeholder) // use the placeholder to replace the token | ||||
| @@ -179,11 +222,11 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string { | ||||
| 	return res.String() | ||||
| } | ||||
|  | ||||
| func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) { | ||||
| func (hcd *highlightCodeDiff) recoverOneDiff(str string) template.HTML { | ||||
| 	sb := strings.Builder{} | ||||
| 	var tagStack []string | ||||
|  | ||||
| 	for _, r := range diff.Text { | ||||
| 	for _, r := range str { | ||||
| 		token, ok := hcd.placeholderTokenMap[r] | ||||
| 		if !ok || token == "" { | ||||
| 			sb.WriteRune(r) // if the rune is not a placeholder, write it as it is | ||||
| @@ -217,6 +260,5 @@ func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) { | ||||
| 			} // else: impossible. every tag was pushed into the stack by the code above and is valid HTML opening tag | ||||
| 		} | ||||
| 	} | ||||
|  | ||||
| 	diff.Text = sb.String() | ||||
| 	return template.HTML(sb.String()) | ||||
| } | ||||
|   | ||||
| @@ -5,121 +5,72 @@ package gitdiff | ||||
|  | ||||
| import ( | ||||
| 	"fmt" | ||||
| 	"html/template" | ||||
| 	"strings" | ||||
| 	"testing" | ||||
|  | ||||
| 	"github.com/sergi/go-diff/diffmatchpatch" | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| ) | ||||
|  | ||||
| func TestDiffWithHighlight(t *testing.T) { | ||||
| 	hcd := newHighlightCodeDiff() | ||||
| 	diffs := hcd.diffWithHighlight( | ||||
| 		"main.v", "", | ||||
| 		"		run('<>')\n", | ||||
| 		"		run(db)\n", | ||||
| 	) | ||||
| 	t.Run("DiffLineAddDel", func(t *testing.T) { | ||||
| 		hcd := newHighlightCodeDiff() | ||||
| 		codeA := template.HTML(`x <span class="k">foo</span> y`) | ||||
| 		codeB := template.HTML(`x <span class="k">bar</span> y`) | ||||
| 		outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB) | ||||
| 		assert.Equal(t, `x <span class="k"><span class="removed-code">foo</span></span> y`, string(outDel)) | ||||
| 		outAdd := hcd.diffLineWithHighlight(DiffLineAdd, codeA, codeB) | ||||
| 		assert.Equal(t, `x <span class="k"><span class="added-code">bar</span></span> y`, string(outAdd)) | ||||
| 	}) | ||||
|  | ||||
| 	expected := `		<span class="n">run</span><span class="o">(</span><span class="removed-code"><span class="k">'</span><span class="o"><</span><span class="o">></span><span class="k">'</span></span><span class="o">)</span>` | ||||
| 	output := diffToHTML(nil, diffs, DiffLineDel) | ||||
| 	assert.Equal(t, expected, output) | ||||
|  | ||||
| 	expected = `		<span class="n">run</span><span class="o">(</span><span class="added-code"><span class="n">db</span></span><span class="o">)</span>` | ||||
| 	output = diffToHTML(nil, diffs, DiffLineAdd) | ||||
| 	assert.Equal(t, expected, output) | ||||
|  | ||||
| 	hcd = newHighlightCodeDiff() | ||||
| 	hcd.placeholderTokenMap['O'] = "<span>" | ||||
| 	hcd.placeholderTokenMap['C'] = "</span>" | ||||
| 	diff := diffmatchpatch.Diff{} | ||||
|  | ||||
| 	diff.Text = "OC" | ||||
| 	hcd.recoverOneDiff(&diff) | ||||
| 	assert.Equal(t, "<span></span>", diff.Text) | ||||
|  | ||||
| 	diff.Text = "O" | ||||
| 	hcd.recoverOneDiff(&diff) | ||||
| 	assert.Equal(t, "<span></span>", diff.Text) | ||||
|  | ||||
| 	diff.Text = "C" | ||||
| 	hcd.recoverOneDiff(&diff) | ||||
| 	assert.Equal(t, "", diff.Text) | ||||
| 	t.Run("OpenCloseTags", func(t *testing.T) { | ||||
| 		hcd := newHighlightCodeDiff() | ||||
| 		hcd.placeholderTokenMap['O'], hcd.placeholderTokenMap['C'] = "<span>", "</span>" | ||||
| 		assert.Equal(t, "<span></span>", string(hcd.recoverOneDiff("OC"))) | ||||
| 		assert.Equal(t, "<span></span>", string(hcd.recoverOneDiff("O"))) | ||||
| 		assert.Equal(t, "", string(hcd.recoverOneDiff("C"))) | ||||
| 	}) | ||||
| } | ||||
|  | ||||
| func TestDiffWithHighlightPlaceholder(t *testing.T) { | ||||
| 	hcd := newHighlightCodeDiff() | ||||
| 	diffs := hcd.diffWithHighlight( | ||||
| 		"main.js", "", | ||||
| 		"a='\U00100000'", | ||||
| 		"a='\U0010FFFD''", | ||||
| 	) | ||||
| 	output := hcd.diffLineWithHighlight(DiffLineDel, "a='\U00100000'", "a='\U0010FFFD''") | ||||
| 	assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000]) | ||||
| 	assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD]) | ||||
|  | ||||
| 	expected := fmt.Sprintf(`<span class="nx">a</span><span class="o">=</span><span class="s1">'</span><span class="removed-code">%s</span>'`, "\U00100000") | ||||
| 	output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel) | ||||
| 	assert.Equal(t, expected, output) | ||||
| 	expected := fmt.Sprintf(`a='<span class="removed-code">%s</span>'`, "\U00100000") | ||||
| 	assert.Equal(t, expected, string(output)) | ||||
|  | ||||
| 	hcd = newHighlightCodeDiff() | ||||
| 	diffs = hcd.diffWithHighlight( | ||||
| 		"main.js", "", | ||||
| 		"a='\U00100000'", | ||||
| 		"a='\U0010FFFD'", | ||||
| 	) | ||||
| 	expected = fmt.Sprintf(`<span class="nx">a</span><span class="o">=</span><span class="s1">'</span><span class="added-code">%s</span>'`, "\U0010FFFD") | ||||
| 	output = diffToHTML(nil, diffs, DiffLineAdd) | ||||
| 	assert.Equal(t, expected, output) | ||||
| 	output = hcd.diffLineWithHighlight(DiffLineAdd, "a='\U00100000'", "a='\U0010FFFD'") | ||||
| 	expected = fmt.Sprintf(`a='<span class="added-code">%s</span>'`, "\U0010FFFD") | ||||
| 	assert.Equal(t, expected, string(output)) | ||||
| } | ||||
|  | ||||
| func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) { | ||||
| 	hcd := newHighlightCodeDiff() | ||||
| 	hcd.placeholderMaxCount = 0 | ||||
| 	diffs := hcd.diffWithHighlight( | ||||
| 		"main.js", "", | ||||
| 		"'", | ||||
| 		``, | ||||
| 	) | ||||
| 	output := diffToHTML(nil, diffs, DiffLineDel) | ||||
| 	expected := fmt.Sprintf(`<span class="removed-code">%s#39;</span>`, "\uFFFD") | ||||
| 	assert.Equal(t, expected, output) | ||||
|  | ||||
| 	hcd = newHighlightCodeDiff() | ||||
| 	hcd.placeholderMaxCount = 0 | ||||
| 	diffs = hcd.diffWithHighlight( | ||||
| 		"main.js", "", | ||||
| 		"a < b", | ||||
| 		"a > b", | ||||
| 	) | ||||
| 	output = diffToHTML(nil, diffs, DiffLineDel) | ||||
| 	expected = fmt.Sprintf(`a %s<span class="removed-code">l</span>t; b`, "\uFFFD") | ||||
| 	assert.Equal(t, expected, output) | ||||
|  | ||||
| 	output = diffToHTML(nil, diffs, DiffLineAdd) | ||||
| 	expected = fmt.Sprintf(`a %s<span class="added-code">g</span>t; b`, "\uFFFD") | ||||
| 	assert.Equal(t, expected, output) | ||||
| 	placeHolderAmp := string(rune(0xFFFD)) | ||||
| 	output := hcd.diffLineWithHighlight(DiffLineDel, `<span class="k"><</span>`, `<span class="k">></span>`) | ||||
| 	assert.Equal(t, placeHolderAmp+"lt;", string(output)) | ||||
| 	output = hcd.diffLineWithHighlight(DiffLineAdd, `<span class="k"><</span>`, `<span class="k">></span>`) | ||||
| 	assert.Equal(t, placeHolderAmp+"gt;", string(output)) | ||||
| } | ||||
|  | ||||
| func TestDiffWithHighlightTagMatch(t *testing.T) { | ||||
| 	totalOverflow := 0 | ||||
| 	for i := 0; i < 100; i++ { | ||||
| 		hcd := newHighlightCodeDiff() | ||||
| 		hcd.placeholderMaxCount = i | ||||
| 		diffs := hcd.diffWithHighlight( | ||||
| 			"main.js", "", | ||||
| 			"a='1'", | ||||
| 			"b='2'", | ||||
| 		) | ||||
| 		totalOverflow += hcd.placeholderOverflowCount | ||||
|  | ||||
| 		output := diffToHTML(nil, diffs, DiffLineDel) | ||||
| 		c1 := strings.Count(output, "<span") | ||||
| 		c2 := strings.Count(output, "</span") | ||||
| 		assert.Equal(t, c1, c2) | ||||
|  | ||||
| 		output = diffToHTML(nil, diffs, DiffLineAdd) | ||||
| 		c1 = strings.Count(output, "<span") | ||||
| 		c2 = strings.Count(output, "</span") | ||||
| 		assert.Equal(t, c1, c2) | ||||
| 	f := func(t *testing.T, lineType DiffLineType) { | ||||
| 		totalOverflow := 0 | ||||
| 		for i := 0; ; i++ { | ||||
| 			hcd := newHighlightCodeDiff() | ||||
| 			hcd.placeholderMaxCount = i | ||||
| 			output := string(hcd.diffLineWithHighlight(lineType, `<span class="k"><</span>`, `<span class="k">></span>`)) | ||||
| 			totalOverflow += hcd.placeholderOverflowCount | ||||
| 			assert.Equal(t, strings.Count(output, "<span"), strings.Count(output, "</span")) | ||||
| 			if hcd.placeholderOverflowCount == 0 { | ||||
| 				break | ||||
| 			} | ||||
| 		} | ||||
| 		assert.NotZero(t, totalOverflow) | ||||
| 	} | ||||
| 	assert.NotZero(t, totalOverflow) | ||||
| 	t.Run("DiffLineAdd", func(t *testing.T) { f(t, DiffLineAdd) }) | ||||
| 	t.Run("DiffLineDel", func(t *testing.T) { f(t, DiffLineDel) }) | ||||
| } | ||||
|   | ||||
| @@ -35,7 +35,6 @@ func TestGetDiffPreview(t *testing.T) { | ||||
| 				Name:        "README.md", | ||||
| 				OldName:     "README.md", | ||||
| 				NameHash:    "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d", | ||||
| 				Index:       1, | ||||
| 				Addition:    2, | ||||
| 				Deletion:    1, | ||||
| 				Type:        2, | ||||
|   | ||||
| @@ -9,15 +9,15 @@ | ||||
| 		> | ||||
| 			<overflow-menu class="ui secondary pointing tabular menu custom"> | ||||
| 				<div class="overflow-menu-items tw-justify-center"> | ||||
| 					<a class="item active" data-tab="diff-side-by-side-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a> | ||||
| 					<a class="item active" data-tab="diff-side-by-side-{{.file.NameHash}}">{{ctx.Locale.Tr "repo.diff.image.side_by_side"}}</a> | ||||
| 					{{if and .blobBase .blobHead}} | ||||
| 					<a class="item" data-tab="diff-swipe-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.swipe"}}</a> | ||||
| 					<a class="item" data-tab="diff-overlay-{{.file.Index}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a> | ||||
| 					<a class="item" data-tab="diff-swipe-{{.file.NameHash}}">{{ctx.Locale.Tr "repo.diff.image.swipe"}}</a> | ||||
| 					<a class="item" data-tab="diff-overlay-{{.file.NameHash}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a> | ||||
| 					{{end}} | ||||
| 				</div> | ||||
| 			</overflow-menu> | ||||
| 			<div class="image-diff-tabs is-loading"> | ||||
| 				<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.Index}}"> | ||||
| 				<div class="ui bottom attached tab image-diff-container active" data-tab="diff-side-by-side-{{.file.NameHash}}"> | ||||
| 					<div class="diff-side-by-side"> | ||||
| 						{{if .blobBase}} | ||||
| 						<span class="side"> | ||||
| @@ -52,7 +52,7 @@ | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				{{if and .blobBase .blobHead}} | ||||
| 				<div class="ui bottom attached tab image-diff-container" data-tab="diff-swipe-{{.file.Index}}"> | ||||
| 				<div class="ui bottom attached tab image-diff-container" data-tab="diff-swipe-{{.file.NameHash}}"> | ||||
| 					<div class="diff-swipe"> | ||||
| 						<div class="swipe-frame"> | ||||
| 							<span class="before-container"><img class="image-before"></span> | ||||
| @@ -66,7 +66,7 @@ | ||||
| 						</div> | ||||
| 					</div> | ||||
| 				</div> | ||||
| 				<div class="ui bottom attached tab image-diff-container" data-tab="diff-overlay-{{.file.Index}}"> | ||||
| 				<div class="ui bottom attached tab image-diff-container" data-tab="diff-overlay-{{.file.NameHash}}"> | ||||
| 					<div class="diff-overlay"> | ||||
| 						<input type="range" min="0" max="100" value="50"> | ||||
| 						<div class="overlay-frame"> | ||||
|   | ||||
| @@ -5,30 +5,33 @@ package integration | ||||
|  | ||||
| import ( | ||||
| 	"net/http" | ||||
| 	"net/url" | ||||
| 	"testing" | ||||
|  | ||||
| 	pull_service "code.gitea.io/gitea/services/pull" | ||||
|  | ||||
| 	"github.com/stretchr/testify/assert" | ||||
| 	"github.com/stretchr/testify/require" | ||||
| ) | ||||
|  | ||||
| func TestListPullCommits(t *testing.T) { | ||||
| 	onGiteaRun(t, func(t *testing.T, u *url.URL) { | ||||
| 		session := loginUser(t, "user5") | ||||
| 		req := NewRequest(t, "GET", "/user2/repo1/pulls/3/commits/list") | ||||
| 		resp := session.MakeRequest(t, req, http.StatusOK) | ||||
| 	session := loginUser(t, "user5") | ||||
| 	req := NewRequest(t, "GET", "/user2/repo1/pulls/3/commits/list") | ||||
| 	resp := session.MakeRequest(t, req, http.StatusOK) | ||||
|  | ||||
| 		var pullCommitList struct { | ||||
| 			Commits             []pull_service.CommitInfo `json:"commits"` | ||||
| 			LastReviewCommitSha string                    `json:"last_review_commit_sha"` | ||||
| 		} | ||||
| 		DecodeJSON(t, resp, &pullCommitList) | ||||
| 	var pullCommitList struct { | ||||
| 		Commits             []pull_service.CommitInfo `json:"commits"` | ||||
| 		LastReviewCommitSha string                    `json:"last_review_commit_sha"` | ||||
| 	} | ||||
| 	DecodeJSON(t, resp, &pullCommitList) | ||||
|  | ||||
| 		if assert.Len(t, pullCommitList.Commits, 2) { | ||||
| 			assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID) | ||||
| 			assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID) | ||||
| 		} | ||||
| 		assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha) | ||||
| 	require.Len(t, pullCommitList.Commits, 2) | ||||
| 	assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID) | ||||
| 	assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID) | ||||
| 	assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha) | ||||
|  | ||||
| 	t.Run("CommitBlobExcerpt", func(t *testing.T) { | ||||
| 		req = NewRequest(t, "GET", "/user2/repo1/blob_excerpt/985f0301dba5e7b34be866819cd15ad3d8f508ee?last_left=0&last_right=0&left=2&right=2&left_hunk_size=2&right_hunk_size=2&path=README.md&style=split&direction=up") | ||||
| 		resp = session.MakeRequest(t, req, http.StatusOK) | ||||
| 		assert.Contains(t, resp.Body.String(), `<td class="lines-code lines-code-new"><code class="code-inner"># repo1</code>`) | ||||
| 	}) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user