diff.go 3.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161
  1. package format
  2. import (
  3. "bytes"
  4. "fmt"
  5. "strings"
  6. "unicode"
  7. "gotest.tools/internal/difflib"
  8. )
  9. const (
  10. contextLines = 2
  11. )
  12. // DiffConfig for a unified diff
  13. type DiffConfig struct {
  14. A string
  15. B string
  16. From string
  17. To string
  18. }
  19. // UnifiedDiff is a modified version of difflib.WriteUnifiedDiff with better
  20. // support for showing the whitespace differences.
  21. func UnifiedDiff(conf DiffConfig) string {
  22. a := strings.SplitAfter(conf.A, "\n")
  23. b := strings.SplitAfter(conf.B, "\n")
  24. groups := difflib.NewMatcher(a, b).GetGroupedOpCodes(contextLines)
  25. if len(groups) == 0 {
  26. return ""
  27. }
  28. buf := new(bytes.Buffer)
  29. writeFormat := func(format string, args ...interface{}) {
  30. buf.WriteString(fmt.Sprintf(format, args...))
  31. }
  32. writeLine := func(prefix string, s string) {
  33. buf.WriteString(prefix + s)
  34. }
  35. if hasWhitespaceDiffLines(groups, a, b) {
  36. writeLine = visibleWhitespaceLine(writeLine)
  37. }
  38. formatHeader(writeFormat, conf)
  39. for _, group := range groups {
  40. formatRangeLine(writeFormat, group)
  41. for _, opCode := range group {
  42. in, out := a[opCode.I1:opCode.I2], b[opCode.J1:opCode.J2]
  43. switch opCode.Tag {
  44. case 'e':
  45. formatLines(writeLine, " ", in)
  46. case 'r':
  47. formatLines(writeLine, "-", in)
  48. formatLines(writeLine, "+", out)
  49. case 'd':
  50. formatLines(writeLine, "-", in)
  51. case 'i':
  52. formatLines(writeLine, "+", out)
  53. }
  54. }
  55. }
  56. return buf.String()
  57. }
  58. // hasWhitespaceDiffLines returns true if any diff groups is only different
  59. // because of whitespace characters.
  60. func hasWhitespaceDiffLines(groups [][]difflib.OpCode, a, b []string) bool {
  61. for _, group := range groups {
  62. in, out := new(bytes.Buffer), new(bytes.Buffer)
  63. for _, opCode := range group {
  64. if opCode.Tag == 'e' {
  65. continue
  66. }
  67. for _, line := range a[opCode.I1:opCode.I2] {
  68. in.WriteString(line)
  69. }
  70. for _, line := range b[opCode.J1:opCode.J2] {
  71. out.WriteString(line)
  72. }
  73. }
  74. if removeWhitespace(in.String()) == removeWhitespace(out.String()) {
  75. return true
  76. }
  77. }
  78. return false
  79. }
  80. func removeWhitespace(s string) string {
  81. var result []rune
  82. for _, r := range s {
  83. if !unicode.IsSpace(r) {
  84. result = append(result, r)
  85. }
  86. }
  87. return string(result)
  88. }
  89. func visibleWhitespaceLine(ws func(string, string)) func(string, string) {
  90. mapToVisibleSpace := func(r rune) rune {
  91. switch r {
  92. case '\n':
  93. case ' ':
  94. return '·'
  95. case '\t':
  96. return '▷'
  97. case '\v':
  98. return '▽'
  99. case '\r':
  100. return '↵'
  101. case '\f':
  102. return '↓'
  103. default:
  104. if unicode.IsSpace(r) {
  105. return '�'
  106. }
  107. }
  108. return r
  109. }
  110. return func(prefix, s string) {
  111. ws(prefix, strings.Map(mapToVisibleSpace, s))
  112. }
  113. }
  114. func formatHeader(wf func(string, ...interface{}), conf DiffConfig) {
  115. if conf.From != "" || conf.To != "" {
  116. wf("--- %s\n", conf.From)
  117. wf("+++ %s\n", conf.To)
  118. }
  119. }
  120. func formatRangeLine(wf func(string, ...interface{}), group []difflib.OpCode) {
  121. first, last := group[0], group[len(group)-1]
  122. range1 := formatRangeUnified(first.I1, last.I2)
  123. range2 := formatRangeUnified(first.J1, last.J2)
  124. wf("@@ -%s +%s @@\n", range1, range2)
  125. }
  126. // Convert range to the "ed" format
  127. func formatRangeUnified(start, stop int) string {
  128. // Per the diff spec at http://www.unix.org/single_unix_specification/
  129. beginning := start + 1 // lines start numbering with one
  130. length := stop - start
  131. if length == 1 {
  132. return fmt.Sprintf("%d", beginning)
  133. }
  134. if length == 0 {
  135. beginning-- // empty ranges begin at line just before the range
  136. }
  137. return fmt.Sprintf("%d,%d", beginning, length)
  138. }
  139. func formatLines(writeLine func(string, string), prefix string, lines []string) {
  140. for _, line := range lines {
  141. writeLine(prefix, line)
  142. }
  143. // Add a newline if the last line is missing one so that the diff displays
  144. // properly.
  145. if !strings.HasSuffix(lines[len(lines)-1], "\n") {
  146. writeLine("", "\n")
  147. }
  148. }