diff.go 3.8 KB

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