diff.go 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191
  1. package gitutil
  2. import (
  3. "bytes"
  4. "html"
  5. "html/template"
  6. "io"
  7. "sync"
  8. "github.com/cockroachdb/errors"
  9. "github.com/sergi/go-diff/diffmatchpatch"
  10. "golang.org/x/net/html/charset"
  11. "golang.org/x/text/transform"
  12. "github.com/gogs/git-module"
  13. "gogs.io/gogs/internal/conf"
  14. "gogs.io/gogs/internal/template/highlight"
  15. "gogs.io/gogs/internal/tool"
  16. )
  17. // DiffSection is a wrapper to git.DiffSection with helper methods.
  18. type DiffSection struct {
  19. *git.DiffSection
  20. initOnce sync.Once
  21. dmp *diffmatchpatch.DiffMatchPatch
  22. }
  23. // ComputedInlineDiffFor computes inline diff for the given line.
  24. func (s *DiffSection) ComputedInlineDiffFor(line *git.DiffLine) template.HTML {
  25. fallback := template.HTML(html.EscapeString(line.Content))
  26. if conf.Git.DisableDiffHighlight {
  27. return fallback
  28. }
  29. // Find equivalent diff line, ignore when not found.
  30. var diff1, diff2 string
  31. switch line.Type {
  32. case git.DiffLineAdd:
  33. compareLine := s.Line(git.DiffLineDelete, line.RightLine)
  34. if compareLine == nil {
  35. return fallback
  36. }
  37. diff1 = compareLine.Content
  38. diff2 = line.Content
  39. case git.DiffLineDelete:
  40. compareLine := s.Line(git.DiffLineAdd, line.LeftLine)
  41. if compareLine == nil {
  42. return fallback
  43. }
  44. diff1 = line.Content
  45. diff2 = compareLine.Content
  46. default:
  47. return fallback
  48. }
  49. s.initOnce.Do(func() {
  50. s.dmp = diffmatchpatch.New()
  51. s.dmp.DiffEditCost = 100
  52. })
  53. diffs := s.dmp.DiffMain(diff1[1:], diff2[1:], true)
  54. diffs = s.dmp.DiffCleanupEfficiency(diffs)
  55. return diffsToHTML(diffs, line.Type)
  56. }
  57. func diffsToHTML(diffs []diffmatchpatch.Diff, lineType git.DiffLineType) template.HTML {
  58. buf := bytes.NewBuffer(nil)
  59. // Reproduce signs which are cut for inline diff before.
  60. switch lineType {
  61. case git.DiffLineAdd:
  62. buf.WriteByte('+')
  63. case git.DiffLineDelete:
  64. buf.WriteByte('-')
  65. }
  66. const (
  67. addedCodePrefix = `<span class="added-code">`
  68. removedCodePrefix = `<span class="removed-code">`
  69. codeTagSuffix = `</span>`
  70. )
  71. for i := range diffs {
  72. switch {
  73. case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
  74. buf.WriteString(addedCodePrefix)
  75. buf.WriteString(html.EscapeString(diffs[i].Text))
  76. buf.WriteString(codeTagSuffix)
  77. case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
  78. buf.WriteString(removedCodePrefix)
  79. buf.WriteString(html.EscapeString(diffs[i].Text))
  80. buf.WriteString(codeTagSuffix)
  81. case diffs[i].Type == diffmatchpatch.DiffEqual:
  82. buf.WriteString(html.EscapeString(diffs[i].Text))
  83. }
  84. }
  85. return template.HTML(buf.Bytes())
  86. }
  87. // DiffFile is a wrapper to git.DiffFile with helper methods.
  88. type DiffFile struct {
  89. *git.DiffFile
  90. Sections []*DiffSection
  91. }
  92. // HighlightClass returns the detected highlight class for the file.
  93. func (diffFile *DiffFile) HighlightClass() string {
  94. return highlight.FileNameToHighlightClass(diffFile.Name)
  95. }
  96. // Diff is a wrapper to git.Diff with helper methods.
  97. type Diff struct {
  98. *git.Diff
  99. Files []*DiffFile
  100. }
  101. // NewDiff returns a new wrapper of given git.Diff.
  102. func NewDiff(oldDiff *git.Diff) *Diff {
  103. newDiff := &Diff{
  104. Diff: oldDiff,
  105. Files: make([]*DiffFile, oldDiff.NumFiles()),
  106. }
  107. // FIXME: detect encoding while parsing.
  108. var buf bytes.Buffer
  109. for i := range oldDiff.Files {
  110. buf.Reset()
  111. newDiff.Files[i] = &DiffFile{
  112. DiffFile: oldDiff.Files[i],
  113. Sections: make([]*DiffSection, oldDiff.Files[i].NumSections()),
  114. }
  115. for j := range oldDiff.Files[i].Sections {
  116. newDiff.Files[i].Sections[j] = &DiffSection{
  117. DiffSection: oldDiff.Files[i].Sections[j],
  118. }
  119. for k := range newDiff.Files[i].Sections[j].Lines {
  120. buf.WriteString(newDiff.Files[i].Sections[j].Lines[k].Content)
  121. buf.WriteString("\n")
  122. }
  123. }
  124. charsetLabel, err := tool.DetectEncoding(buf.Bytes())
  125. if charsetLabel != "UTF-8" && err == nil {
  126. encoding, _ := charset.Lookup(charsetLabel)
  127. if encoding != nil {
  128. d := encoding.NewDecoder()
  129. for j := range newDiff.Files[i].Sections {
  130. for k := range newDiff.Files[i].Sections[j].Lines {
  131. if c, _, err := transform.String(d, newDiff.Files[i].Sections[j].Lines[k].Content); err == nil {
  132. newDiff.Files[i].Sections[j].Lines[k].Content = c
  133. }
  134. }
  135. }
  136. }
  137. }
  138. }
  139. return newDiff
  140. }
  141. // ParseDiff parses the diff from given io.Reader.
  142. func ParseDiff(r io.Reader, maxFiles, maxFileLines, maxLineChars int) (*Diff, error) {
  143. done := make(chan git.SteamParseDiffResult)
  144. go git.StreamParseDiff(r, done, maxFiles, maxFileLines, maxLineChars)
  145. result := <-done
  146. if result.Err != nil {
  147. return nil, errors.Newf("stream parse diff: %v", result.Err)
  148. }
  149. return NewDiff(result.Diff), nil
  150. }
  151. // RepoDiff parses the diff on given revisions of given repository.
  152. func RepoDiff(repo *git.Repository, rev string, maxFiles, maxFileLines, maxLineChars int, opts ...git.DiffOptions) (*Diff, error) {
  153. diff, err := repo.Diff(rev, maxFiles, maxFileLines, maxLineChars, opts...)
  154. if err != nil {
  155. return nil, errors.Newf("get diff: %v", err)
  156. }
  157. return NewDiff(diff), nil
  158. }