forked from NYANDEV/forgejo
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
|
||||
}
|
||||
|
||||
var unterminatedEntityRE = regexp.MustCompile(`&[^ ;]*$`)
|
||||
var unstartedEntiyRE = regexp.MustCompile(`^[^ ;]*;`)
|
||||
|
||||
// GetComputedInlineDiffFor computes inline diff for the given line.
|
||||
func (diffSection *DiffSection) GetComputedInlineDiffFor(diffLine *DiffLine) template.HTML {
|
||||
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.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)
|
||||
}
|
||||
|
||||
// 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.
|
||||
type DiffFile struct {
|
||||
Name string
|
||||
|
|
|
@ -15,6 +15,7 @@ import (
|
|||
"code.gitea.io/gitea/models"
|
||||
"code.gitea.io/gitea/modules/git"
|
||||
"code.gitea.io/gitea/modules/setting"
|
||||
"github.com/sergi/go-diff/diffmatchpatch"
|
||||
dmp "github.com/sergi/go-diff/diffmatchpatch"
|
||||
"github.com/stretchr/testify/assert"
|
||||
"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) {
|
||||
setting.Cfg = ini.Empty()
|
||||
assertEqual(t, "foo <span class=\"added-code\">bar</span> biz", diffToHTML("", []dmp.Diff{
|
||||
|
|
Loading…
Reference in a new issue