diff --git a/services/gitdiff/gitdiff.go b/services/gitdiff/gitdiff.go index 50e2f8cf1e..4f223fdc03 100644 --- a/services/gitdiff/gitdiff.go +++ b/services/gitdiff/gitdiff.go @@ -182,6 +182,7 @@ var ( codeTagSuffix = []byte(``) ) var trailingSpanRegex = regexp.MustCompile(`]?$`) +var entityRegex = regexp.MustCompile(`&[#]*?[0-9[:alpha:]]*$`) // shouldWriteInline represents combinations where we manually write inline changes func shouldWriteInline(diff diffmatchpatch.Diff, lineType DiffLineType) bool { @@ -205,14 +206,40 @@ func diffToHTML(fileName string, diffs []diffmatchpatch.Diff, lineType DiffLineT match = "" } // Chroma HTML syntax highlighting is done before diffing individual lines in order to maintain consistency. - // Since inline changes might split in the middle of a chroma span tag, make we manually put it back together - // before writing so we don't try insert added/removed code spans in the middle of an existing chroma span - // and create broken HTML. + // Since inline changes might split in the middle of a chroma span tag or HTML entity, make we manually put it back together + // before writing so we don't try insert added/removed code spans in the middle of one of those + // and create broken HTML. This is done by moving incomplete HTML forward until it no longer matches our pattern of + // a line ending with an incomplete HTML entity or partial/opening . + + // EX: + // diffs[{Type: dmp.DiffDelete, Text: "language}] + + // After first iteration + // diffs[{Type: dmp.DiffDelete, Text: "language"}, //write out + // {Type: dmp.DiffEqual, Text: ",}] + + // After second iteration + // {Type: dmp.DiffEqual, Text: ""}, // write out + // {Type: dmp.DiffDelete, Text: ",}] + + // Final + // {Type: dmp.DiffDelete, Text: ",}] + // end up writing , + // Instead of lass="p", + m := trailingSpanRegex.FindStringSubmatchIndex(diff.Text) if m != nil { match = diff.Text[m[0]:m[1]] diff.Text = strings.TrimSuffix(diff.Text, match) } + m = entityRegex.FindStringSubmatchIndex(diff.Text) + if m != nil { + match = diff.Text[m[0]:m[1]] + diff.Text = strings.TrimSuffix(diff.Text, match) + } // Print an existing closing span first before opening added/remove-code span so it doesn't unintentionally close it if strings.HasPrefix(diff.Text, "") { buf.WriteString("") @@ -290,9 +317,6 @@ 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 { @@ -333,89 +357,11 @@ 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 diff --git a/services/gitdiff/gitdiff_test.go b/services/gitdiff/gitdiff_test.go index 0683997dab..6e3b5b0994 100644 --- a/services/gitdiff/gitdiff_test.go +++ b/services/gitdiff/gitdiff_test.go @@ -15,7 +15,6 @@ 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" @@ -27,35 +26,6 @@ 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 bar biz", diffToHTML("", []dmp.Diff{ @@ -113,6 +83,21 @@ func TestDiffToHTML(t *testing.T) { {Type: dmp.DiffEqual, Text: ""// ", sys.argv"}, {Type: dmp.DiffInsert, Text: ")"}, }, DiffLineAdd)) + + assertEqual(t, "sh 'useradd -u $(stat -c "%u" .gitignore) jenkins'", diffToHTML("", []dmp.Diff{ + {Type: dmp.DiffEqual, Text: "sh "}, + {Type: dmp.DiffDelete, Text: "4;useradd -u 111 jenkins""}, + {Type: dmp.DiffInsert, Text: "9;useradd -u $(stat -c "%u" .gitignore) jenkins'"}, + {Type: dmp.DiffEqual, Text: ";"}, + }, DiffLineAdd)) + + assertEqual(t, " <h4 class="release-list-title df ac">", diffToHTML("", []dmp.Diff{ + {Type: dmp.DiffEqual, Text: " <h"}, + {Type: dmp.DiffInsert, Text: "4 class=&#"}, + {Type: dmp.DiffEqual, Text: "3"}, + {Type: dmp.DiffInsert, Text: "4;release-list-title df ac""}, + {Type: dmp.DiffEqual, Text: ">"}, + }, DiffLineAdd)) } func TestParsePatch_singlefile(t *testing.T) {