diff.go 5.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195
  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. const (
  70. addedCodePrefix = `<span class="added-code">`
  71. removedCodePrefix = `<span class="removed-code">`
  72. codeTagSuffix = `</span>`
  73. )
  74. for i := range diffs {
  75. switch {
  76. case diffs[i].Type == diffmatchpatch.DiffInsert && lineType == git.DiffLineAdd:
  77. buf.WriteString(addedCodePrefix)
  78. buf.WriteString(html.EscapeString(diffs[i].Text))
  79. buf.WriteString(codeTagSuffix)
  80. case diffs[i].Type == diffmatchpatch.DiffDelete && lineType == git.DiffLineDelete:
  81. buf.WriteString(removedCodePrefix)
  82. buf.WriteString(html.EscapeString(diffs[i].Text))
  83. buf.WriteString(codeTagSuffix)
  84. case diffs[i].Type == diffmatchpatch.DiffEqual:
  85. buf.WriteString(html.EscapeString(diffs[i].Text))
  86. }
  87. }
  88. return template.HTML(buf.Bytes())
  89. }
  90. // DiffFile is a wrapper to git.DiffFile with helper methods.
  91. type DiffFile struct {
  92. *git.DiffFile
  93. Sections []*DiffSection
  94. }
  95. // HighlightClass returns the detected highlight class for the file.
  96. func (diffFile *DiffFile) HighlightClass() string {
  97. return highlight.FileNameToHighlightClass(diffFile.Name)
  98. }
  99. // Diff is a wrapper to git.Diff with helper methods.
  100. type Diff struct {
  101. *git.Diff
  102. Files []*DiffFile
  103. }
  104. // NewDiff returns a new wrapper of given git.Diff.
  105. func NewDiff(oldDiff *git.Diff) *Diff {
  106. newDiff := &Diff{
  107. Diff: oldDiff,
  108. Files: make([]*DiffFile, oldDiff.NumFiles()),
  109. }
  110. // FIXME: detect encoding while parsing.
  111. var buf bytes.Buffer
  112. for i := range oldDiff.Files {
  113. buf.Reset()
  114. newDiff.Files[i] = &DiffFile{
  115. DiffFile: oldDiff.Files[i],
  116. Sections: make([]*DiffSection, oldDiff.Files[i].NumSections()),
  117. }
  118. for j := range oldDiff.Files[i].Sections {
  119. newDiff.Files[i].Sections[j] = &DiffSection{
  120. DiffSection: oldDiff.Files[i].Sections[j],
  121. }
  122. for k := range newDiff.Files[i].Sections[j].Lines {
  123. buf.WriteString(newDiff.Files[i].Sections[j].Lines[k].Content)
  124. buf.WriteString("\n")
  125. }
  126. }
  127. charsetLabel, err := tool.DetectEncoding(buf.Bytes())
  128. if charsetLabel != "UTF-8" && err == nil {
  129. encoding, _ := charset.Lookup(charsetLabel)
  130. if encoding != nil {
  131. d := encoding.NewDecoder()
  132. for j := range newDiff.Files[i].Sections {
  133. for k := range newDiff.Files[i].Sections[j].Lines {
  134. if c, _, err := transform.String(d, newDiff.Files[i].Sections[j].Lines[k].Content); err == nil {
  135. newDiff.Files[i].Sections[j].Lines[k].Content = c
  136. }
  137. }
  138. }
  139. }
  140. }
  141. }
  142. return newDiff
  143. }
  144. // ParseDiff parses the diff from given io.Reader.
  145. func ParseDiff(r io.Reader, maxFiles, maxFileLines, maxLineChars int) (*Diff, error) {
  146. done := make(chan git.SteamParseDiffResult)
  147. go git.StreamParseDiff(r, done, maxFiles, maxFileLines, maxLineChars)
  148. result := <-done
  149. if result.Err != nil {
  150. return nil, fmt.Errorf("stream parse diff: %v", result.Err)
  151. }
  152. return NewDiff(result.Diff), nil
  153. }
  154. // RepoDiff parses the diff on given revisions of given repository.
  155. func RepoDiff(repo *git.Repository, rev string, maxFiles, maxFileLines, maxLineChars int, opts ...git.DiffOptions) (*Diff, error) {
  156. diff, err := repo.Diff(rev, maxFiles, maxFileLines, maxLineChars, opts...)
  157. if err != nil {
  158. return nil, fmt.Errorf("get diff: %v", err)
  159. }
  160. return NewDiff(diff), nil
  161. }