mirror of
https://github.com/go-gitea/gitea.git
synced 2025-04-03 05:39:55 +08: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:
parent
6f13331754
commit
3f1f808b9e
@ -7,6 +7,7 @@ package git
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
|
"errors"
|
||||||
"io"
|
"io"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/typesniffer"
|
"code.gitea.io/gitea/modules/typesniffer"
|
||||||
@ -34,8 +35,9 @@ func (b *Blob) GetBlobContent(limit int64) (string, error) {
|
|||||||
return string(buf), err
|
return string(buf), err
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetBlobLineCount gets line count of the blob
|
// GetBlobLineCount gets line count of the blob.
|
||||||
func (b *Blob) GetBlobLineCount() (int, error) {
|
// 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()
|
reader, err := b.DataAsync()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return 0, err
|
return 0, err
|
||||||
@ -44,20 +46,20 @@ func (b *Blob) GetBlobLineCount() (int, error) {
|
|||||||
buf := make([]byte, 32*1024)
|
buf := make([]byte, 32*1024)
|
||||||
count := 1
|
count := 1
|
||||||
lineSep := []byte{'\n'}
|
lineSep := []byte{'\n'}
|
||||||
|
|
||||||
c, err := reader.Read(buf)
|
|
||||||
if c == 0 && err == io.EOF {
|
|
||||||
return 0, nil
|
|
||||||
}
|
|
||||||
for {
|
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)
|
count += bytes.Count(buf[:c], lineSep)
|
||||||
switch {
|
switch {
|
||||||
case err == io.EOF:
|
case errors.Is(err, io.EOF):
|
||||||
return count, nil
|
return count, nil
|
||||||
case err != nil:
|
case err != nil:
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
c, err = reader.Read(buf)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -11,6 +11,7 @@ import (
|
|||||||
gohtml "html"
|
gohtml "html"
|
||||||
"html/template"
|
"html/template"
|
||||||
"io"
|
"io"
|
||||||
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@ -83,7 +84,7 @@ func Code(fileName, language, code string) (output template.HTML, lexerName stri
|
|||||||
}
|
}
|
||||||
|
|
||||||
if lexer == nil {
|
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
|
// use mapped value to find lexer
|
||||||
lexer = lexers.Get(val)
|
lexer = lexers.Get(val)
|
||||||
}
|
}
|
||||||
|
@ -1591,8 +1591,7 @@ func GetPullRequestFiles(ctx *context.APIContext) {
|
|||||||
maxLines := setting.Git.MaxGitDiffLines
|
maxLines := setting.Git.MaxGitDiffLines
|
||||||
|
|
||||||
// FIXME: If there are too many files in the repo, may cause some unpredictable issues.
|
// 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.GetDiffForAPI(ctx, baseGitRepo,
|
||||||
diff, err := gitdiff.GetDiff(ctx, baseGitRepo,
|
|
||||||
&gitdiff.DiffOptions{
|
&gitdiff.DiffOptions{
|
||||||
BeforeCommitID: startCommitID,
|
BeforeCommitID: startCommitID,
|
||||||
AfterCommitID: endCommitID,
|
AfterCommitID: endCommitID,
|
||||||
|
@ -97,7 +97,7 @@ func RefBlame(ctx *context.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx.Data["NumLines"], err = blob.GetBlobLineCount()
|
ctx.Data["NumLines"], err = blob.GetBlobLineCount(nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.NotFound(err)
|
ctx.NotFound(err)
|
||||||
return
|
return
|
||||||
|
@ -314,7 +314,7 @@ func Diff(ctx *context.Context) {
|
|||||||
maxLines, maxFiles = -1, -1
|
maxLines, maxFiles = -1, -1
|
||||||
}
|
}
|
||||||
|
|
||||||
diff, err := gitdiff.GetDiff(ctx, gitRepo, &gitdiff.DiffOptions{
|
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, &gitdiff.DiffOptions{
|
||||||
AfterCommitID: commitID,
|
AfterCommitID: commitID,
|
||||||
SkipTo: ctx.FormString("skip-to"),
|
SkipTo: ctx.FormString("skip-to"),
|
||||||
MaxLines: maxLines,
|
MaxLines: maxLines,
|
||||||
|
@ -614,7 +614,7 @@ func PrepareCompareDiff(
|
|||||||
|
|
||||||
fileOnly := ctx.FormBool("file-only")
|
fileOnly := ctx.FormBool("file-only")
|
||||||
|
|
||||||
diff, err := gitdiff.GetDiff(ctx, ci.HeadGitRepo,
|
diff, err := gitdiff.GetDiffForRender(ctx, ci.HeadGitRepo,
|
||||||
&gitdiff.DiffOptions{
|
&gitdiff.DiffOptions{
|
||||||
BeforeCommitID: beforeCommitID,
|
BeforeCommitID: beforeCommitID,
|
||||||
AfterCommitID: headCommitID,
|
AfterCommitID: headCommitID,
|
||||||
|
@ -749,7 +749,7 @@ func viewPullFiles(ctx *context.Context, specifiedStartCommit, specifiedEndCommi
|
|||||||
diffOptions.BeforeCommitID = startCommitID
|
diffOptions.BeforeCommitID = startCommitID
|
||||||
}
|
}
|
||||||
|
|
||||||
diff, err := gitdiff.GetDiff(ctx, gitRepo, diffOptions, files...)
|
diff, err := gitdiff.GetDiffForRender(ctx, gitRepo, diffOptions, files...)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
ctx.ServerError("GetDiff", err)
|
ctx.ServerError("GetDiff", err)
|
||||||
return
|
return
|
||||||
|
@ -31,6 +31,7 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/optional"
|
"code.gitea.io/gitea/modules/optional"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
"code.gitea.io/gitea/modules/translation"
|
"code.gitea.io/gitea/modules/translation"
|
||||||
|
"code.gitea.io/gitea/modules/util"
|
||||||
|
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
stdcharset "golang.org/x/net/html/charset"
|
stdcharset "golang.org/x/net/html/charset"
|
||||||
@ -75,12 +76,12 @@ const (
|
|||||||
|
|
||||||
// DiffLine represents a line difference in a DiffSection.
|
// DiffLine represents a line difference in a DiffSection.
|
||||||
type DiffLine struct {
|
type DiffLine struct {
|
||||||
LeftIdx int
|
LeftIdx int // line number, 1-based
|
||||||
RightIdx int
|
RightIdx int // line number, 1-based
|
||||||
Match int
|
Match int // line number, 1-based
|
||||||
Type DiffLineType
|
Type DiffLineType
|
||||||
Content string
|
Content string
|
||||||
Comments issues_model.CommentList
|
Comments issues_model.CommentList // related PR code comments
|
||||||
SectionInfo *DiffLineSectionInfo
|
SectionInfo *DiffLineSectionInfo
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -95,9 +96,18 @@ type DiffLineSectionInfo struct {
|
|||||||
RightHunkSize int
|
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
|
// BlobExcerptChunkSize represent max lines of excerpt
|
||||||
const BlobExcerptChunkSize = 20
|
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.
|
// GetType returns the type of DiffLine.
|
||||||
func (d *DiffLine) GetType() int {
|
func (d *DiffLine) GetType() int {
|
||||||
return int(d.Type)
|
return int(d.Type)
|
||||||
@ -112,9 +122,10 @@ func (d *DiffLine) GetHTMLDiffLineType() string {
|
|||||||
return "del"
|
return "del"
|
||||||
case DiffLineSection:
|
case DiffLineSection:
|
||||||
return "tag"
|
return "tag"
|
||||||
}
|
default:
|
||||||
return "same"
|
return "same"
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// CanComment returns whether a line can get commented
|
// CanComment returns whether a line can get commented
|
||||||
func (d *DiffLine) CanComment() bool {
|
func (d *DiffLine) CanComment() bool {
|
||||||
@ -196,38 +207,6 @@ type DiffSection struct {
|
|||||||
Lines []*DiffLine
|
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
|
// GetLine gets a specific line by type (add or del) and file line number
|
||||||
func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
|
func (diffSection *DiffSection) GetLine(lineType DiffLineType, idx int) *DiffLine {
|
||||||
var (
|
var (
|
||||||
@ -271,10 +250,10 @@ LOOP:
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var diffMatchPatch = diffmatchpatch.New()
|
func defaultDiffMatchPatch() *diffmatchpatch.DiffMatchPatch {
|
||||||
|
dmp := diffmatchpatch.New()
|
||||||
func init() {
|
dmp.DiffEditCost = 100
|
||||||
diffMatchPatch.DiffEditCost = 100
|
return dmp
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffInline is a struct that has a content and escape status
|
// DiffInline is a struct that has a content and escape status
|
||||||
@ -283,97 +262,114 @@ type DiffInline struct {
|
|||||||
Content template.HTML
|
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 {
|
func DiffInlineWithUnicodeEscape(s template.HTML, locale translation.Locale) DiffInline {
|
||||||
status, content := charset.EscapeControlHTML(s, locale)
|
status, content := charset.EscapeControlHTML(s, locale)
|
||||||
return DiffInline{EscapeStatus: status, Content: content}
|
return DiffInline{EscapeStatus: status, Content: content}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DiffInlineWithHighlightCode makes a DiffInline with code highlight and hidden unicode characters escaped
|
func (diffSection *DiffSection) getLineContentForRender(lineIdx int, diffLine *DiffLine, fileLanguage string, highlightLines map[int]template.HTML) template.HTML {
|
||||||
func DiffInlineWithHighlightCode(fileName, language, code string, locale translation.Locale) DiffInline {
|
h, ok := highlightLines[lineIdx-1]
|
||||||
highlighted, _ := highlight.Code(fileName, language, code)
|
if ok {
|
||||||
status, content := charset.EscapeControlHTML(highlighted, locale)
|
return h
|
||||||
return DiffInline{EscapeStatus: status, Content: content}
|
}
|
||||||
|
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.
|
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine, locale translation.Locale) DiffInline {
|
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
|
// try to find equivalent diff line. ignore, otherwise
|
||||||
switch diffLine.Type {
|
switch diffLine.Type {
|
||||||
case DiffLineSection:
|
case DiffLineSection:
|
||||||
return getLineContent(diffLine.Content[1:], locale)
|
return getLineContent(diffLine.Content[1:], locale)
|
||||||
case DiffLineAdd:
|
case DiffLineAdd:
|
||||||
compareDiffLine = diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
|
compareDiffLine := diffSection.GetLine(DiffLineDel, diffLine.RightIdx)
|
||||||
if compareDiffLine == nil {
|
return diffSection.getDiffLineForRender(DiffLineAdd, compareDiffLine, diffLine, locale)
|
||||||
return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], locale)
|
|
||||||
}
|
|
||||||
diff1 = compareDiffLine.Content
|
|
||||||
diff2 = diffLine.Content
|
|
||||||
case DiffLineDel:
|
case DiffLineDel:
|
||||||
compareDiffLine = diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
|
compareDiffLine := diffSection.GetLine(DiffLineAdd, diffLine.LeftIdx)
|
||||||
if compareDiffLine == nil {
|
return diffSection.getDiffLineForRender(DiffLineDel, diffLine, compareDiffLine, locale)
|
||||||
return DiffInlineWithHighlightCode(diffSection.FileName, language, diffLine.Content[1:], 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)
|
||||||
}
|
}
|
||||||
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.
|
// DiffFile represents a file diff.
|
||||||
type DiffFile struct {
|
type DiffFile struct {
|
||||||
|
// only used internally to parse Ambiguous filenames
|
||||||
|
isAmbiguous bool
|
||||||
|
|
||||||
|
// basic fields (parsed from diff result)
|
||||||
Name string
|
Name string
|
||||||
NameHash string
|
NameHash string
|
||||||
OldName string
|
OldName string
|
||||||
Index int
|
Addition int
|
||||||
Addition, Deletion int
|
Deletion int
|
||||||
Type DiffFileType
|
Type DiffFileType
|
||||||
|
Mode string
|
||||||
|
OldMode string
|
||||||
IsCreated bool
|
IsCreated bool
|
||||||
IsDeleted bool
|
IsDeleted bool
|
||||||
IsBin bool
|
IsBin bool
|
||||||
IsLFSFile bool
|
IsLFSFile bool
|
||||||
IsRenamed bool
|
IsRenamed bool
|
||||||
IsAmbiguous bool
|
IsSubmodule bool
|
||||||
|
// basic fields but for render purpose only
|
||||||
Sections []*DiffSection
|
Sections []*DiffSection
|
||||||
IsIncomplete bool
|
IsIncomplete bool
|
||||||
IsIncompleteLineTooLong bool
|
IsIncompleteLineTooLong bool
|
||||||
IsProtected bool
|
|
||||||
|
// will be filled by the extra loop in GitDiffForRender
|
||||||
|
Language string
|
||||||
IsGenerated bool
|
IsGenerated bool
|
||||||
IsVendored 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
|
IsViewed bool // User specific
|
||||||
HasChangedSinceLastReview bool // User specific
|
HasChangedSinceLastReview bool // User specific
|
||||||
Language string
|
|
||||||
Mode string
|
|
||||||
OldMode string
|
|
||||||
|
|
||||||
IsSubmodule bool // if IsSubmodule==true, then there must be a SubmoduleDiffInfo
|
// for render purpose only, will be filled by the extra loop in GitDiffForRender
|
||||||
SubmoduleDiffInfo *SubmoduleDiffInfo
|
highlightedLeftLines map[int]template.HTML
|
||||||
|
highlightedRightLines map[int]template.HTML
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetType returns type of diff file.
|
// GetType returns type of diff file.
|
||||||
@ -381,18 +377,23 @@ func (diffFile *DiffFile) GetType() int {
|
|||||||
return int(diffFile.Type)
|
return int(diffFile.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetTailSection creates a fake DiffLineSection if the last section is not the end of the file
|
type DiffLimitedContent struct {
|
||||||
func (diffFile *DiffFile) GetTailSection(leftCommit, rightCommit *git.Commit) *DiffSection {
|
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 {
|
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]
|
lastSection := diffFile.Sections[len(diffFile.Sections)-1]
|
||||||
lastLine := lastSection.Lines[len(lastSection.Lines)-1]
|
lastLine := lastSection.Lines[len(lastSection.Lines)-1]
|
||||||
leftLineCount := getCommitFileLineCount(leftCommit, diffFile.Name)
|
leftLineCount, leftContent := getCommitFileLineCountAndLimitedContent(leftCommit, diffFile.Name)
|
||||||
rightLineCount := getCommitFileLineCount(rightCommit, diffFile.Name)
|
rightLineCount, rightContent := getCommitFileLineCountAndLimitedContent(rightCommit, diffFile.Name)
|
||||||
|
diffLimitedContent = DiffLimitedContent{LeftContent: leftContent, RightContent: rightContent}
|
||||||
if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
|
if leftLineCount <= lastLine.LeftIdx || rightLineCount <= lastLine.RightIdx {
|
||||||
return nil
|
return nil, diffLimitedContent
|
||||||
}
|
}
|
||||||
tailDiffLine := &DiffLine{
|
tailDiffLine := &DiffLine{
|
||||||
Type: DiffLineSection,
|
Type: DiffLineSection,
|
||||||
@ -406,7 +407,7 @@ func (diffFile *DiffFile) GetTailSection(leftCommit, rightCommit *git.Commit) *D
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
tailSection := &DiffSection{FileName: diffFile.Name, Lines: []*DiffLine{tailDiffLine}}
|
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
|
// 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)
|
blob, err := commit.GetBlobByPath(filePath)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return 0
|
return 0, nil
|
||||||
}
|
}
|
||||||
return lineCount
|
return lineCount, w
|
||||||
}
|
}
|
||||||
|
|
||||||
// Diff represents a difference between two git trees.
|
// Diff represents a difference between two git trees.
|
||||||
@ -526,13 +540,13 @@ parsingLoop:
|
|||||||
}
|
}
|
||||||
|
|
||||||
if maxFiles > -1 && len(diff.Files) >= maxFiles {
|
if maxFiles > -1 && len(diff.Files) >= maxFiles {
|
||||||
lastFile := createDiffFile(diff, line)
|
lastFile := createDiffFile(line)
|
||||||
diff.End = lastFile.Name
|
diff.End = lastFile.Name
|
||||||
diff.IsIncomplete = true
|
diff.IsIncomplete = true
|
||||||
break parsingLoop
|
break parsingLoop
|
||||||
}
|
}
|
||||||
|
|
||||||
curFile = createDiffFile(diff, line)
|
curFile = createDiffFile(line)
|
||||||
if skipping {
|
if skipping {
|
||||||
if curFile.Name != skipToFile {
|
if curFile.Name != skipToFile {
|
||||||
line, err = skipToNextDiffHead(input)
|
line, err = skipToNextDiffHead(input)
|
||||||
@ -615,28 +629,28 @@ parsingLoop:
|
|||||||
case strings.HasPrefix(line, "rename from "):
|
case strings.HasPrefix(line, "rename from "):
|
||||||
curFile.IsRenamed = true
|
curFile.IsRenamed = true
|
||||||
curFile.Type = DiffFileRename
|
curFile.Type = DiffFileRename
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
curFile.OldName = prepareValue(line, "rename from ")
|
curFile.OldName = prepareValue(line, "rename from ")
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(line, "rename to "):
|
case strings.HasPrefix(line, "rename to "):
|
||||||
curFile.IsRenamed = true
|
curFile.IsRenamed = true
|
||||||
curFile.Type = DiffFileRename
|
curFile.Type = DiffFileRename
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
curFile.Name = prepareValue(line, "rename to ")
|
curFile.Name = prepareValue(line, "rename to ")
|
||||||
curFile.IsAmbiguous = false
|
curFile.isAmbiguous = false
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(line, "copy from "):
|
case strings.HasPrefix(line, "copy from "):
|
||||||
curFile.IsRenamed = true
|
curFile.IsRenamed = true
|
||||||
curFile.Type = DiffFileCopy
|
curFile.Type = DiffFileCopy
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
curFile.OldName = prepareValue(line, "copy from ")
|
curFile.OldName = prepareValue(line, "copy from ")
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(line, "copy to "):
|
case strings.HasPrefix(line, "copy to "):
|
||||||
curFile.IsRenamed = true
|
curFile.IsRenamed = true
|
||||||
curFile.Type = DiffFileCopy
|
curFile.Type = DiffFileCopy
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
curFile.Name = prepareValue(line, "copy to ")
|
curFile.Name = prepareValue(line, "copy to ")
|
||||||
curFile.IsAmbiguous = false
|
curFile.isAmbiguous = false
|
||||||
}
|
}
|
||||||
case strings.HasPrefix(line, "new file"):
|
case strings.HasPrefix(line, "new file"):
|
||||||
curFile.Type = DiffFileAdd
|
curFile.Type = DiffFileAdd
|
||||||
@ -663,7 +677,7 @@ parsingLoop:
|
|||||||
curFile.IsBin = true
|
curFile.IsBin = true
|
||||||
case strings.HasPrefix(line, "--- "):
|
case strings.HasPrefix(line, "--- "):
|
||||||
// Handle ambiguous filenames
|
// Handle ambiguous filenames
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
// The shortest string that can end up here is:
|
// The shortest string that can end up here is:
|
||||||
// "--- a\t\n" without the quotes.
|
// "--- a\t\n" without the quotes.
|
||||||
// This line has a len() of 7 but doesn't contain a oldName.
|
// This line has a len() of 7 but doesn't contain a oldName.
|
||||||
@ -681,7 +695,7 @@ parsingLoop:
|
|||||||
// Otherwise do nothing with this line
|
// Otherwise do nothing with this line
|
||||||
case strings.HasPrefix(line, "+++ "):
|
case strings.HasPrefix(line, "+++ "):
|
||||||
// Handle ambiguous filenames
|
// Handle ambiguous filenames
|
||||||
if curFile.IsAmbiguous {
|
if curFile.isAmbiguous {
|
||||||
if len(line) > 6 && line[4] == 'b' {
|
if len(line) > 6 && line[4] == 'b' {
|
||||||
curFile.Name = line[6 : len(line)-1]
|
curFile.Name = line[6 : len(line)-1]
|
||||||
if line[len(line)-2] == '\t' {
|
if line[len(line)-2] == '\t' {
|
||||||
@ -693,7 +707,7 @@ parsingLoop:
|
|||||||
} else {
|
} else {
|
||||||
curFile.Name = curFile.OldName
|
curFile.Name = curFile.OldName
|
||||||
}
|
}
|
||||||
curFile.IsAmbiguous = false
|
curFile.isAmbiguous = false
|
||||||
}
|
}
|
||||||
// Otherwise do nothing with this line, but now switch to parsing hunks
|
// Otherwise do nothing with this line, but now switch to parsing hunks
|
||||||
lineBytes, isFragment, err := parseHunks(ctx, curFile, maxLines, maxLineCharacters, input)
|
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.
|
// 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
|
// Especially, even for a creation or a deletion, /dev/null is not used
|
||||||
// in place of the a/ or b/ filenames.
|
// 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.
|
// 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...
|
// there is potential ambiguity...
|
||||||
//
|
//
|
||||||
// but we can be simpler with our heuristics by just forcing git to prefix things nicely
|
// but we can be simpler with our heuristics by just forcing git to prefix things nicely
|
||||||
curFile := &DiffFile{
|
curFile := &DiffFile{
|
||||||
Index: len(diff.Files) + 1,
|
|
||||||
Type: DiffFileChange,
|
Type: DiffFileChange,
|
||||||
Sections: make([]*DiffSection, 0, 10),
|
Sections: make([]*DiffSection, 0, 10),
|
||||||
}
|
}
|
||||||
@ -1034,7 +1047,7 @@ func createDiffFile(diff *Diff, line string) *DiffFile {
|
|||||||
curFile.OldName, oldNameAmbiguity = readFileName(rd)
|
curFile.OldName, oldNameAmbiguity = readFileName(rd)
|
||||||
curFile.Name, newNameAmbiguity = readFileName(rd)
|
curFile.Name, newNameAmbiguity = readFileName(rd)
|
||||||
if oldNameAmbiguity && newNameAmbiguity {
|
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
|
// 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 ...
|
// So we need to start again ...
|
||||||
if (len(line)-len(cmdDiffHead)-1)%2 == 0 {
|
if (len(line)-len(cmdDiffHead)-1)%2 == 0 {
|
||||||
@ -1121,20 +1134,21 @@ func guessBeforeCommitForDiff(gitRepo *git.Repository, beforeCommitID string, af
|
|||||||
return actualBeforeCommit, actualBeforeCommitID, nil
|
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.
|
// Passing the empty string as beforeCommitID returns a diff from the parent commit.
|
||||||
// The whitespaceBehavior is either an empty string or a git flag
|
// 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
|
repoPath := gitRepo.Path
|
||||||
|
|
||||||
afterCommit, err := gitRepo.GetCommit(opts.AfterCommitID)
|
afterCommit, err = gitRepo.GetCommit(opts.AfterCommitID)
|
||||||
if err != nil {
|
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, nil, nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdDiff := git.NewCommand().
|
cmdDiff := git.NewCommand().
|
||||||
@ -1150,7 +1164,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
|
|||||||
parsePatchSkipToFile = ""
|
parsePatchSkipToFile = ""
|
||||||
}
|
}
|
||||||
|
|
||||||
cmdDiff.AddDynamicArguments(actualBeforeCommitID.String(), opts.AfterCommitID)
|
cmdDiff.AddDynamicArguments(beforeCommitID.String(), opts.AfterCommitID)
|
||||||
cmdDiff.AddDashesAndList(files...)
|
cmdDiff.AddDashesAndList(files...)
|
||||||
|
|
||||||
cmdCtx, cmdCancel := context.WithCancel(ctx)
|
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
|
// Ensure the git process is killed if it didn't exit already
|
||||||
cmdCancel()
|
cmdCancel()
|
||||||
if err != nil {
|
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
|
diff.Start = opts.SkipTo
|
||||||
|
return diff, beforeCommit, afterCommit, nil
|
||||||
|
}
|
||||||
|
|
||||||
checker, deferable := gitRepo.CheckAttributeReader(opts.AfterCommitID)
|
func GetDiffForAPI(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, files ...string) (*Diff, error) {
|
||||||
defer deferable()
|
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 {
|
for _, diffFile := range diff.Files {
|
||||||
isVendored := optional.None[bool]()
|
isVendored := optional.None[bool]()
|
||||||
@ -1205,7 +1232,7 @@ func GetDiff(ctx context.Context, gitRepo *git.Repository, opts *DiffOptions, fi
|
|||||||
|
|
||||||
// Populate Submodule URLs
|
// Populate Submodule URLs
|
||||||
if diffFile.SubmoduleDiffInfo != nil {
|
if diffFile.SubmoduleDiffInfo != nil {
|
||||||
diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, actualBeforeCommit, afterCommit)
|
diffFile.SubmoduleDiffInfo.PopulateURL(diffFile, beforeCommit, afterCommit)
|
||||||
}
|
}
|
||||||
|
|
||||||
if !isVendored.Has() {
|
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))
|
isGenerated = optional.Some(analyze.IsGenerated(diffFile.Name))
|
||||||
}
|
}
|
||||||
diffFile.IsGenerated = isGenerated.Value()
|
diffFile.IsGenerated = isGenerated.Value()
|
||||||
|
tailSection, limitedContent := diffFile.GetTailSectionAndLimitedContent(beforeCommit, afterCommit)
|
||||||
tailSection := diffFile.GetTailSection(actualBeforeCommit, afterCommit)
|
|
||||||
if tailSection != nil {
|
if tailSection != nil {
|
||||||
diffFile.Sections = append(diffFile.Sections, tailSection)
|
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
|
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 {
|
type DiffShortStat struct {
|
||||||
NumFiles, TotalAddition, TotalDeletion int
|
NumFiles, TotalAddition, TotalDeletion int
|
||||||
}
|
}
|
||||||
|
@ -17,27 +17,10 @@ import (
|
|||||||
"code.gitea.io/gitea/modules/json"
|
"code.gitea.io/gitea/modules/json"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
|
||||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"github.com/stretchr/testify/require"
|
"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) {
|
func TestParsePatch_skipTo(t *testing.T) {
|
||||||
type testcase struct {
|
type testcase struct {
|
||||||
name string
|
name string
|
||||||
@ -621,7 +604,7 @@ func TestGetDiffRangeWithWhitespaceBehavior(t *testing.T) {
|
|||||||
|
|
||||||
defer gitRepo.Close()
|
defer gitRepo.Close()
|
||||||
for _, behavior := range []git.TrustedCmdArgs{{"-w"}, {"--ignore-space-at-eol"}, {"-b"}, nil} {
|
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{
|
&DiffOptions{
|
||||||
AfterCommitID: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd",
|
AfterCommitID: "d8e0bbb45f200e67d9a784ce55bd90821af45ebd",
|
||||||
BeforeCommitID: "72866af952e98d02a73003501836074b286a78f6",
|
BeforeCommitID: "72866af952e98d02a73003501836074b286a78f6",
|
||||||
|
@ -4,10 +4,10 @@
|
|||||||
package gitdiff
|
package gitdiff
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"code.gitea.io/gitea/modules/highlight"
|
|
||||||
|
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
"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)
|
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 {
|
for _, r := range code {
|
||||||
if hcd.isInPlaceholderRange(r) {
|
if hcd.isInPlaceholderRange(r) {
|
||||||
// put the existing rune (used by code) in map, then this rune won't be used a placeholder anymore.
|
// 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(codeA)
|
||||||
hcd.collectUsedRunes(codeB)
|
hcd.collectUsedRunes(codeB)
|
||||||
|
|
||||||
highlightCodeA, _ := highlight.Code(filename, language, codeA)
|
convertedCodeA := hcd.convertToPlaceholders(codeA)
|
||||||
highlightCodeB, _ := highlight.Code(filename, language, codeB)
|
convertedCodeB := hcd.convertToPlaceholders(codeB)
|
||||||
|
|
||||||
convertedCodeA := hcd.convertToPlaceholders(string(highlightCodeA))
|
dmp := defaultDiffMatchPatch()
|
||||||
convertedCodeB := hcd.convertToPlaceholders(string(highlightCodeB))
|
diffs := dmp.DiffMain(convertedCodeA, convertedCodeB, true)
|
||||||
|
diffs = dmp.DiffCleanupEfficiency(diffs)
|
||||||
|
|
||||||
diffs := diffMatchPatch.DiffMain(convertedCodeA, convertedCodeB, true)
|
buf := bytes.NewBuffer(nil)
|
||||||
diffs = diffMatchPatch.DiffCleanupEfficiency(diffs)
|
|
||||||
|
|
||||||
for i := range diffs {
|
// restore the line wrapper tags <span class="line"> and <span class="cl">, if necessary
|
||||||
hcd.recoverOneDiff(&diffs[i])
|
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.
|
// 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
|
var tagStack []string
|
||||||
res := strings.Builder{}
|
res := strings.Builder{}
|
||||||
|
|
||||||
@ -115,6 +164,7 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
|
|||||||
var beforeToken, token string
|
var beforeToken, token string
|
||||||
var valid bool
|
var valid bool
|
||||||
|
|
||||||
|
htmlCode := string(htmlContent)
|
||||||
// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>"
|
// the standard chroma highlight HTML is "<span class="line [hl]"><span class="cl"> ... </span></span>"
|
||||||
for {
|
for {
|
||||||
beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode)
|
beforeToken, token, htmlCode, valid = extractHTMLToken(htmlCode)
|
||||||
@ -151,14 +201,7 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
|
|||||||
} // else: impossible
|
} // else: impossible
|
||||||
|
|
||||||
// remember the placeholder and token in the map
|
// remember the placeholder and token in the map
|
||||||
placeholder, ok := hcd.tokenPlaceholderMap[tokenInMap]
|
placeholder := hcd.registerTokenAsPlaceholder(tokenInMap)
|
||||||
if !ok {
|
|
||||||
placeholder = hcd.nextPlaceholder()
|
|
||||||
if placeholder != 0 {
|
|
||||||
hcd.tokenPlaceholderMap[tokenInMap] = placeholder
|
|
||||||
hcd.placeholderTokenMap[placeholder] = tokenInMap
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if placeholder != 0 {
|
if placeholder != 0 {
|
||||||
res.WriteRune(placeholder) // use the placeholder to replace the token
|
res.WriteRune(placeholder) // use the placeholder to replace the token
|
||||||
@ -179,11 +222,11 @@ func (hcd *highlightCodeDiff) convertToPlaceholders(htmlCode string) string {
|
|||||||
return res.String()
|
return res.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (hcd *highlightCodeDiff) recoverOneDiff(diff *diffmatchpatch.Diff) {
|
func (hcd *highlightCodeDiff) recoverOneDiff(str string) template.HTML {
|
||||||
sb := strings.Builder{}
|
sb := strings.Builder{}
|
||||||
var tagStack []string
|
var tagStack []string
|
||||||
|
|
||||||
for _, r := range diff.Text {
|
for _, r := range str {
|
||||||
token, ok := hcd.placeholderTokenMap[r]
|
token, ok := hcd.placeholderTokenMap[r]
|
||||||
if !ok || token == "" {
|
if !ok || token == "" {
|
||||||
sb.WriteRune(r) // if the rune is not a placeholder, write it as it is
|
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
|
} // else: impossible. every tag was pushed into the stack by the code above and is valid HTML opening tag
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
return template.HTML(sb.String())
|
||||||
diff.Text = sb.String()
|
|
||||||
}
|
}
|
||||||
|
@ -5,121 +5,72 @@ package gitdiff
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"html/template"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/sergi/go-diff/diffmatchpatch"
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestDiffWithHighlight(t *testing.T) {
|
func TestDiffWithHighlight(t *testing.T) {
|
||||||
|
t.Run("DiffLineAddDel", func(t *testing.T) {
|
||||||
hcd := newHighlightCodeDiff()
|
hcd := newHighlightCodeDiff()
|
||||||
diffs := hcd.diffWithHighlight(
|
codeA := template.HTML(`x <span class="k">foo</span> y`)
|
||||||
"main.v", "",
|
codeB := template.HTML(`x <span class="k">bar</span> y`)
|
||||||
" run('<>')\n",
|
outDel := hcd.diffLineWithHighlight(DiffLineDel, codeA, codeB)
|
||||||
" run(db)\n",
|
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>`
|
t.Run("OpenCloseTags", func(t *testing.T) {
|
||||||
output := diffToHTML(nil, diffs, DiffLineDel)
|
hcd := newHighlightCodeDiff()
|
||||||
assert.Equal(t, expected, output)
|
hcd.placeholderTokenMap['O'], hcd.placeholderTokenMap['C'] = "<span>", "</span>"
|
||||||
|
assert.Equal(t, "<span></span>", string(hcd.recoverOneDiff("OC")))
|
||||||
expected = ` <span class="n">run</span><span class="o">(</span><span class="added-code"><span class="n">db</span></span><span class="o">)</span>`
|
assert.Equal(t, "<span></span>", string(hcd.recoverOneDiff("O")))
|
||||||
output = diffToHTML(nil, diffs, DiffLineAdd)
|
assert.Equal(t, "", string(hcd.recoverOneDiff("C")))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiffWithHighlightPlaceholder(t *testing.T) {
|
func TestDiffWithHighlightPlaceholder(t *testing.T) {
|
||||||
hcd := newHighlightCodeDiff()
|
hcd := newHighlightCodeDiff()
|
||||||
diffs := hcd.diffWithHighlight(
|
output := hcd.diffLineWithHighlight(DiffLineDel, "a='\U00100000'", "a='\U0010FFFD''")
|
||||||
"main.js", "",
|
|
||||||
"a='\U00100000'",
|
|
||||||
"a='\U0010FFFD''",
|
|
||||||
)
|
|
||||||
assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000])
|
assert.Equal(t, "", hcd.placeholderTokenMap[0x00100000])
|
||||||
assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD])
|
assert.Equal(t, "", hcd.placeholderTokenMap[0x0010FFFD])
|
||||||
|
expected := fmt.Sprintf(`a='<span class="removed-code">%s</span>'`, "\U00100000")
|
||||||
expected := fmt.Sprintf(`<span class="nx">a</span><span class="o">=</span><span class="s1">'</span><span class="removed-code">%s</span>'`, "\U00100000")
|
assert.Equal(t, expected, string(output))
|
||||||
output := diffToHTML(hcd.lineWrapperTags, diffs, DiffLineDel)
|
|
||||||
assert.Equal(t, expected, output)
|
|
||||||
|
|
||||||
hcd = newHighlightCodeDiff()
|
hcd = newHighlightCodeDiff()
|
||||||
diffs = hcd.diffWithHighlight(
|
output = hcd.diffLineWithHighlight(DiffLineAdd, "a='\U00100000'", "a='\U0010FFFD'")
|
||||||
"main.js", "",
|
expected = fmt.Sprintf(`a='<span class="added-code">%s</span>'`, "\U0010FFFD")
|
||||||
"a='\U00100000'",
|
assert.Equal(t, expected, string(output))
|
||||||
"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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) {
|
func TestDiffWithHighlightPlaceholderExhausted(t *testing.T) {
|
||||||
hcd := newHighlightCodeDiff()
|
hcd := newHighlightCodeDiff()
|
||||||
hcd.placeholderMaxCount = 0
|
hcd.placeholderMaxCount = 0
|
||||||
diffs := hcd.diffWithHighlight(
|
placeHolderAmp := string(rune(0xFFFD))
|
||||||
"main.js", "",
|
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))
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestDiffWithHighlightTagMatch(t *testing.T) {
|
func TestDiffWithHighlightTagMatch(t *testing.T) {
|
||||||
|
f := func(t *testing.T, lineType DiffLineType) {
|
||||||
totalOverflow := 0
|
totalOverflow := 0
|
||||||
for i := 0; i < 100; i++ {
|
for i := 0; ; i++ {
|
||||||
hcd := newHighlightCodeDiff()
|
hcd := newHighlightCodeDiff()
|
||||||
hcd.placeholderMaxCount = i
|
hcd.placeholderMaxCount = i
|
||||||
diffs := hcd.diffWithHighlight(
|
output := string(hcd.diffLineWithHighlight(lineType, `<span class="k"><</span>`, `<span class="k">></span>`))
|
||||||
"main.js", "",
|
|
||||||
"a='1'",
|
|
||||||
"b='2'",
|
|
||||||
)
|
|
||||||
totalOverflow += hcd.placeholderOverflowCount
|
totalOverflow += hcd.placeholderOverflowCount
|
||||||
|
assert.Equal(t, strings.Count(output, "<span"), strings.Count(output, "</span"))
|
||||||
output := diffToHTML(nil, diffs, DiffLineDel)
|
if hcd.placeholderOverflowCount == 0 {
|
||||||
c1 := strings.Count(output, "<span")
|
break
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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",
|
Name: "README.md",
|
||||||
OldName: "README.md",
|
OldName: "README.md",
|
||||||
NameHash: "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d",
|
NameHash: "8ec9a00bfd09b3190ac6b22251dbb1aa95a0579d",
|
||||||
Index: 1,
|
|
||||||
Addition: 2,
|
Addition: 2,
|
||||||
Deletion: 1,
|
Deletion: 1,
|
||||||
Type: 2,
|
Type: 2,
|
||||||
|
@ -9,15 +9,15 @@
|
|||||||
>
|
>
|
||||||
<overflow-menu class="ui secondary pointing tabular menu custom">
|
<overflow-menu class="ui secondary pointing tabular menu custom">
|
||||||
<div class="overflow-menu-items tw-justify-center">
|
<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}}
|
{{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-swipe-{{.file.NameHash}}">{{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-overlay-{{.file.NameHash}}">{{ctx.Locale.Tr "repo.diff.image.overlay"}}</a>
|
||||||
{{end}}
|
{{end}}
|
||||||
</div>
|
</div>
|
||||||
</overflow-menu>
|
</overflow-menu>
|
||||||
<div class="image-diff-tabs is-loading">
|
<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">
|
<div class="diff-side-by-side">
|
||||||
{{if .blobBase}}
|
{{if .blobBase}}
|
||||||
<span class="side">
|
<span class="side">
|
||||||
@ -52,7 +52,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
{{if and .blobBase .blobHead}}
|
{{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="diff-swipe">
|
||||||
<div class="swipe-frame">
|
<div class="swipe-frame">
|
||||||
<span class="before-container"><img class="image-before"></span>
|
<span class="before-container"><img class="image-before"></span>
|
||||||
@ -66,7 +66,7 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</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">
|
<div class="diff-overlay">
|
||||||
<input type="range" min="0" max="100" value="50">
|
<input type="range" min="0" max="100" value="50">
|
||||||
<div class="overlay-frame">
|
<div class="overlay-frame">
|
||||||
|
@ -5,16 +5,15 @@ package integration
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
pull_service "code.gitea.io/gitea/services/pull"
|
pull_service "code.gitea.io/gitea/services/pull"
|
||||||
|
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
|
"github.com/stretchr/testify/require"
|
||||||
)
|
)
|
||||||
|
|
||||||
func TestListPullCommits(t *testing.T) {
|
func TestListPullCommits(t *testing.T) {
|
||||||
onGiteaRun(t, func(t *testing.T, u *url.URL) {
|
|
||||||
session := loginUser(t, "user5")
|
session := loginUser(t, "user5")
|
||||||
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/commits/list")
|
req := NewRequest(t, "GET", "/user2/repo1/pulls/3/commits/list")
|
||||||
resp := session.MakeRequest(t, req, http.StatusOK)
|
resp := session.MakeRequest(t, req, http.StatusOK)
|
||||||
@ -25,10 +24,14 @@ func TestListPullCommits(t *testing.T) {
|
|||||||
}
|
}
|
||||||
DecodeJSON(t, resp, &pullCommitList)
|
DecodeJSON(t, resp, &pullCommitList)
|
||||||
|
|
||||||
if assert.Len(t, pullCommitList.Commits, 2) {
|
require.Len(t, pullCommitList.Commits, 2)
|
||||||
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID)
|
assert.Equal(t, "985f0301dba5e7b34be866819cd15ad3d8f508ee", pullCommitList.Commits[0].ID)
|
||||||
assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID)
|
assert.Equal(t, "5c050d3b6d2db231ab1f64e324f1b6b9a0b181c2", pullCommitList.Commits[1].ID)
|
||||||
}
|
|
||||||
assert.Equal(t, "4a357436d925b5c974181ff12a994538ddc5a269", pullCommitList.LastReviewCommitSha)
|
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>`)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user