diff.go 5.0 KB

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