When creating line diffs do not split within an html entity (#13357)
* When creating line diffs do not split within an html entity Fix #13342 Signed-off-by: Andrew Thornton <art27@cantab.net> * Add test case Signed-off-by: Andrew Thornton <art27@cantab.net> * improve test Signed-off-by: Andrew Thornton <art27@cantab.net> Co-authored-by: techknowlogick <techknowlogick@gitea.io>
This commit is contained in:
parent
a420beda2a
commit
6b7c199f5f
2 changed files with 114 additions and 0 deletions
|
@ -290,6 +290,9 @@ func init() {
|
||||||
diffMatchPatch.DiffEditCost = 100
|
diffMatchPatch.DiffEditCost = 100
|
||||||
}
|
}
|
||||||
|
|
||||||
|
var unterminatedEntityRE = regexp.MustCompile(`&[^ ;]*$`)
|
||||||
|
var unstartedEntiyRE = regexp.MustCompile(`^[^ ;]*;`)
|
||||||
|
|
||||||
// GetComputedInlineDiffFor computes inline diff for the given line.
|
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
|
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
|
||||||
if setting.Git.DisableDiffHighlight {
|
if setting.Git.DisableDiffHighlight {
|
||||||
|
@ -329,9 +332,90 @@ func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) tem
|
||||||
|
|
||||||
diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
|
diffRecord := diffMatchPatch.DiffMain(highlight.Code(diffSection.FileName, diff1[1:]), highlight.Code(diffSection.FileName, diff2[1:]), true)
|
||||||
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
||||||
|
|
||||||
|
// Now we need to clean up the split entities
|
||||||
|
diffRecord = unsplitEntities(diffRecord)
|
||||||
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
||||||
|
|
||||||
return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
|
return diffToHTML(diffSection.FileName, diffRecord, diffLine.Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// unsplitEntities looks for broken up html entities. It relies on records being presimplified and the data being passed in being valid html
|
||||||
|
func unsplitEntities(records []diffmatchpatch.Diff) []diffmatchpatch.Diff {
|
||||||
|
// Unsplitting entities is simple...
|
||||||
|
//
|
||||||
|
// Iterate through all be the last records because if we're the last record then there's nothing we can do
|
||||||
|
for i := 0; i+1 < len(records); i++ {
|
||||||
|
record := &records[i]
|
||||||
|
|
||||||
|
// Look for an unterminated entity at the end of the line
|
||||||
|
unterminated := unterminatedEntityRE.FindString(record.Text)
|
||||||
|
if len(unterminated) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
switch record.Type {
|
||||||
|
case diffmatchpatch.DiffEqual:
|
||||||
|
// If we're an diff equal we want to give this unterminated entity to our next delete and insert
|
||||||
|
record.Text = record.Text[0 : len(record.Text)-len(unterminated)]
|
||||||
|
records[i+1].Text = unterminated + records[i+1].Text
|
||||||
|
|
||||||
|
nextType := records[i+1].Type
|
||||||
|
|
||||||
|
if nextType == diffmatchpatch.DiffEqual {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the next in line is a delete then we will want the thing after that to be an insert and so on.
|
||||||
|
oneAfterType := diffmatchpatch.DiffInsert
|
||||||
|
if nextType == diffmatchpatch.DiffInsert {
|
||||||
|
oneAfterType = diffmatchpatch.DiffDelete
|
||||||
|
}
|
||||||
|
|
||||||
|
if i+2 < len(records) && records[i+2].Type == oneAfterType {
|
||||||
|
records[i+2].Text = unterminated + records[i+2].Text
|
||||||
|
} else {
|
||||||
|
records = append(records[:i+2], append([]diffmatchpatch.Diff{
|
||||||
|
{
|
||||||
|
Type: oneAfterType,
|
||||||
|
Text: unterminated,
|
||||||
|
}}, records[i+2:]...)...)
|
||||||
|
}
|
||||||
|
case diffmatchpatch.DiffDelete:
|
||||||
|
fallthrough
|
||||||
|
case diffmatchpatch.DiffInsert:
|
||||||
|
// if we're an insert or delete we want to claim the terminal bit of the entity from the next equal in line
|
||||||
|
targetType := diffmatchpatch.DiffInsert
|
||||||
|
if record.Type == diffmatchpatch.DiffInsert {
|
||||||
|
targetType = diffmatchpatch.DiffDelete
|
||||||
|
}
|
||||||
|
next := &records[i+1]
|
||||||
|
if next.Type == diffmatchpatch.DiffEqual {
|
||||||
|
// if the next is an equal we need to snaffle the entity end off the start and add an delete/insert
|
||||||
|
if terminal := unstartedEntiyRE.FindString(next.Text); len(terminal) > 0 {
|
||||||
|
record.Text += terminal
|
||||||
|
next.Text = next.Text[len(terminal):]
|
||||||
|
records = append(records[:i+2], append([]diffmatchpatch.Diff{
|
||||||
|
{
|
||||||
|
Type: targetType,
|
||||||
|
Text: unterminated,
|
||||||
|
}}, records[i+2:]...)...)
|
||||||
|
}
|
||||||
|
} else if next.Type == targetType {
|
||||||
|
// if the next is an insert we need to snaffle the entity end off the one after that and add it to both.
|
||||||
|
if i+2 < len(records) && records[i+2].Type == diffmatchpatch.DiffEqual {
|
||||||
|
if terminal := unstartedEntiyRE.FindString(records[i+2].Text); len(terminal) > 0 {
|
||||||
|
record.Text += terminal
|
||||||
|
next.Text += terminal
|
||||||
|
records[i+2].Text = records[i+2].Text[len(terminal):]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return records
|
||||||
|
}
|
||||||
|
|
||||||
// DiffFile represents a file diff.
|
// DiffFile represents a file diff.
|
||||||
type DiffFile struct {
|
type DiffFile struct {
|
||||||
Name string
|
Name string
|
||||||
|
|
|
@ -15,6 +15,7 @@ import (
|
||||||
"code.gitea.io/gitea/models"
|
"code.gitea.io/gitea/models"
|
||||||
"code.gitea.io/gitea/modules/git"
|
"code.gitea.io/gitea/modules/git"
|
||||||
"code.gitea.io/gitea/modules/setting"
|
"code.gitea.io/gitea/modules/setting"
|
||||||
|
"github.com/sergi/go-diff/diffmatchpatch"
|
||||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||||
"github.com/stretchr/testify/assert"
|
"github.com/stretchr/testify/assert"
|
||||||
"gopkg.in/ini.v1"
|
"gopkg.in/ini.v1"
|
||||||
|
@ -26,6 +27,35 @@ func assertEqual(t *testing.T, s1 string, s2 template.HTML) {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestUnsplitEntities(t *testing.T) {
|
||||||
|
left := "sh "useradd -u 111 jenkins""
|
||||||
|
right := "sh 'useradd -u $(stat -c "%u" .gitignore) jenkins'"
|
||||||
|
diffRecord := diffMatchPatch.DiffMain(left, right, true)
|
||||||
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
||||||
|
|
||||||
|
// Now we need to clean up the split entities
|
||||||
|
diffRecord = unsplitEntities(diffRecord)
|
||||||
|
diffRecord = diffMatchPatch.DiffCleanupEfficiency(diffRecord)
|
||||||
|
|
||||||
|
leftRecombined := ""
|
||||||
|
rightRecombined := ""
|
||||||
|
for _, record := range diffRecord {
|
||||||
|
assert.False(t, unterminatedEntityRE.MatchString(record.Text), "")
|
||||||
|
switch record.Type {
|
||||||
|
case diffmatchpatch.DiffDelete:
|
||||||
|
leftRecombined += record.Text
|
||||||
|
case diffmatchpatch.DiffInsert:
|
||||||
|
rightRecombined += record.Text
|
||||||
|
default:
|
||||||
|
leftRecombined += record.Text
|
||||||
|
rightRecombined += record.Text
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
assert.EqualValues(t, left, leftRecombined)
|
||||||
|
assert.EqualValues(t, right, rightRecombined)
|
||||||
|
}
|
||||||
|
|
||||||
func TestDiffToHTML(t *testing.T) {
|
func TestDiffToHTML(t *testing.T) {
|
||||||
setting.Cfg = ini.Empty()
|
setting.Cfg = ini.Empty()
|
||||||
assertEqual(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML("", []dmp.Diff{
|
assertEqual(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML("", []dmp.Diff{
|
||||||
|
|
Reference in a new issue