You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

unified_encoder.go 8.5KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. package diff
  2. import (
  3. "fmt"
  4. "io"
  5. "regexp"
  6. "strconv"
  7. "strings"
  8. "github.com/go-git/go-git/v5/plumbing"
  9. )
  10. // DefaultContextLines is the default number of context lines.
  11. const DefaultContextLines = 3
  12. var (
  13. splitLinesRegexp = regexp.MustCompile(`[^\n]*(\n|$)`)
  14. operationChar = map[Operation]byte{
  15. Add: '+',
  16. Delete: '-',
  17. Equal: ' ',
  18. }
  19. operationColorKey = map[Operation]ColorKey{
  20. Add: New,
  21. Delete: Old,
  22. Equal: Context,
  23. }
  24. )
  25. // UnifiedEncoder encodes an unified diff into the provided Writer. It does not
  26. // support similarity index for renames or sorting hash representations.
  27. type UnifiedEncoder struct {
  28. io.Writer
  29. // contextLines is the count of unchanged lines that will appear surrounding
  30. // a change.
  31. contextLines int
  32. // colorConfig is the color configuration. The default is no color.
  33. color ColorConfig
  34. }
  35. // NewUnifiedEncoder returns a new UnifiedEncoder that writes to w.
  36. func NewUnifiedEncoder(w io.Writer, contextLines int) *UnifiedEncoder {
  37. return &UnifiedEncoder{
  38. Writer: w,
  39. contextLines: contextLines,
  40. }
  41. }
  42. // SetColor sets e's color configuration and returns e.
  43. func (e *UnifiedEncoder) SetColor(colorConfig ColorConfig) *UnifiedEncoder {
  44. e.color = colorConfig
  45. return e
  46. }
  47. // Encode encodes patch.
  48. func (e *UnifiedEncoder) Encode(patch Patch) error {
  49. sb := &strings.Builder{}
  50. if message := patch.Message(); message != "" {
  51. sb.WriteString(message)
  52. if !strings.HasSuffix(message, "\n") {
  53. sb.WriteByte('\n')
  54. }
  55. }
  56. for _, filePatch := range patch.FilePatches() {
  57. e.writeFilePatchHeader(sb, filePatch)
  58. g := newHunksGenerator(filePatch.Chunks(), e.contextLines)
  59. for _, hunk := range g.Generate() {
  60. hunk.writeTo(sb, e.color)
  61. }
  62. }
  63. _, err := e.Write([]byte(sb.String()))
  64. return err
  65. }
  66. func (e *UnifiedEncoder) writeFilePatchHeader(sb *strings.Builder, filePatch FilePatch) {
  67. from, to := filePatch.Files()
  68. if from == nil && to == nil {
  69. return
  70. }
  71. isBinary := filePatch.IsBinary()
  72. var lines []string
  73. switch {
  74. case from != nil && to != nil:
  75. hashEquals := from.Hash() == to.Hash()
  76. lines = append(lines,
  77. fmt.Sprintf("diff --git a/%s b/%s", from.Path(), to.Path()),
  78. )
  79. if from.Mode() != to.Mode() {
  80. lines = append(lines,
  81. fmt.Sprintf("old mode %o", from.Mode()),
  82. fmt.Sprintf("new mode %o", to.Mode()),
  83. )
  84. }
  85. if from.Path() != to.Path() {
  86. lines = append(lines,
  87. fmt.Sprintf("rename from %s", from.Path()),
  88. fmt.Sprintf("rename to %s", to.Path()),
  89. )
  90. }
  91. if from.Mode() != to.Mode() && !hashEquals {
  92. lines = append(lines,
  93. fmt.Sprintf("index %s..%s", from.Hash(), to.Hash()),
  94. )
  95. } else if !hashEquals {
  96. lines = append(lines,
  97. fmt.Sprintf("index %s..%s %o", from.Hash(), to.Hash(), from.Mode()),
  98. )
  99. }
  100. if !hashEquals {
  101. lines = e.appendPathLines(lines, "a/"+from.Path(), "b/"+to.Path(), isBinary)
  102. }
  103. case from == nil:
  104. lines = append(lines,
  105. fmt.Sprintf("diff --git a/%s b/%s", to.Path(), to.Path()),
  106. fmt.Sprintf("new file mode %o", to.Mode()),
  107. fmt.Sprintf("index %s..%s", plumbing.ZeroHash, to.Hash()),
  108. )
  109. lines = e.appendPathLines(lines, "/dev/null", "b/"+to.Path(), isBinary)
  110. case to == nil:
  111. lines = append(lines,
  112. fmt.Sprintf("diff --git a/%s b/%s", from.Path(), from.Path()),
  113. fmt.Sprintf("deleted file mode %o", from.Mode()),
  114. fmt.Sprintf("index %s..%s", from.Hash(), plumbing.ZeroHash),
  115. )
  116. lines = e.appendPathLines(lines, "a/"+from.Path(), "/dev/null", isBinary)
  117. }
  118. sb.WriteString(e.color[Meta])
  119. sb.WriteString(lines[0])
  120. for _, line := range lines[1:] {
  121. sb.WriteByte('\n')
  122. sb.WriteString(line)
  123. }
  124. sb.WriteString(e.color.Reset(Meta))
  125. sb.WriteByte('\n')
  126. }
  127. func (e *UnifiedEncoder) appendPathLines(lines []string, fromPath, toPath string, isBinary bool) []string {
  128. if isBinary {
  129. return append(lines,
  130. fmt.Sprintf("Binary files %s and %s differ", fromPath, toPath),
  131. )
  132. }
  133. return append(lines,
  134. fmt.Sprintf("--- %s", fromPath),
  135. fmt.Sprintf("+++ %s", toPath),
  136. )
  137. }
  138. type hunksGenerator struct {
  139. fromLine, toLine int
  140. ctxLines int
  141. chunks []Chunk
  142. current *hunk
  143. hunks []*hunk
  144. beforeContext, afterContext []string
  145. }
  146. func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
  147. return &hunksGenerator{
  148. chunks: chunks,
  149. ctxLines: ctxLines,
  150. }
  151. }
  152. func (g *hunksGenerator) Generate() []*hunk {
  153. for i, chunk := range g.chunks {
  154. lines := splitLines(chunk.Content())
  155. nLines := len(lines)
  156. switch chunk.Type() {
  157. case Equal:
  158. g.fromLine += nLines
  159. g.toLine += nLines
  160. g.processEqualsLines(lines, i)
  161. case Delete:
  162. if nLines != 0 {
  163. g.fromLine++
  164. }
  165. g.processHunk(i, chunk.Type())
  166. g.fromLine += nLines - 1
  167. g.current.AddOp(chunk.Type(), lines...)
  168. case Add:
  169. if nLines != 0 {
  170. g.toLine++
  171. }
  172. g.processHunk(i, chunk.Type())
  173. g.toLine += nLines - 1
  174. g.current.AddOp(chunk.Type(), lines...)
  175. }
  176. if i == len(g.chunks)-1 && g.current != nil {
  177. g.hunks = append(g.hunks, g.current)
  178. }
  179. }
  180. return g.hunks
  181. }
  182. func (g *hunksGenerator) processHunk(i int, op Operation) {
  183. if g.current != nil {
  184. return
  185. }
  186. var ctxPrefix string
  187. linesBefore := len(g.beforeContext)
  188. if linesBefore > g.ctxLines {
  189. ctxPrefix = g.beforeContext[linesBefore-g.ctxLines-1]
  190. g.beforeContext = g.beforeContext[linesBefore-g.ctxLines:]
  191. linesBefore = g.ctxLines
  192. }
  193. g.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
  194. g.current.AddOp(Equal, g.beforeContext...)
  195. switch op {
  196. case Delete:
  197. g.current.fromLine, g.current.toLine =
  198. g.addLineNumbers(g.fromLine, g.toLine, linesBefore, i, Add)
  199. case Add:
  200. g.current.toLine, g.current.fromLine =
  201. g.addLineNumbers(g.toLine, g.fromLine, linesBefore, i, Delete)
  202. }
  203. g.beforeContext = nil
  204. }
  205. // addLineNumbers obtains the line numbers in a new chunk.
  206. func (g *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
  207. cla = la - linesBefore
  208. // we need to search for a reference for the next diff
  209. switch {
  210. case linesBefore != 0 && g.ctxLines != 0:
  211. if lb > g.ctxLines {
  212. clb = lb - g.ctxLines + 1
  213. } else {
  214. clb = 1
  215. }
  216. case g.ctxLines == 0:
  217. clb = lb
  218. case i != len(g.chunks)-1:
  219. next := g.chunks[i+1]
  220. if next.Type() == op || next.Type() == Equal {
  221. // this diff will be into this chunk
  222. clb = lb + 1
  223. }
  224. }
  225. return
  226. }
  227. func (g *hunksGenerator) processEqualsLines(ls []string, i int) {
  228. if g.current == nil {
  229. g.beforeContext = append(g.beforeContext, ls...)
  230. return
  231. }
  232. g.afterContext = append(g.afterContext, ls...)
  233. if len(g.afterContext) <= g.ctxLines*2 && i != len(g.chunks)-1 {
  234. g.current.AddOp(Equal, g.afterContext...)
  235. g.afterContext = nil
  236. } else {
  237. ctxLines := g.ctxLines
  238. if ctxLines > len(g.afterContext) {
  239. ctxLines = len(g.afterContext)
  240. }
  241. g.current.AddOp(Equal, g.afterContext[:ctxLines]...)
  242. g.hunks = append(g.hunks, g.current)
  243. g.current = nil
  244. g.beforeContext = g.afterContext[ctxLines:]
  245. g.afterContext = nil
  246. }
  247. }
  248. func splitLines(s string) []string {
  249. out := splitLinesRegexp.FindAllString(s, -1)
  250. if out[len(out)-1] == "" {
  251. out = out[:len(out)-1]
  252. }
  253. return out
  254. }
  255. type hunk struct {
  256. fromLine int
  257. toLine int
  258. fromCount int
  259. toCount int
  260. ctxPrefix string
  261. ops []*op
  262. }
  263. func (h *hunk) writeTo(sb *strings.Builder, color ColorConfig) {
  264. sb.WriteString(color[Frag])
  265. sb.WriteString("@@ -")
  266. if h.fromCount == 1 {
  267. sb.WriteString(strconv.Itoa(h.fromLine))
  268. } else {
  269. sb.WriteString(strconv.Itoa(h.fromLine))
  270. sb.WriteByte(',')
  271. sb.WriteString(strconv.Itoa(h.fromCount))
  272. }
  273. sb.WriteString(" +")
  274. if h.toCount == 1 {
  275. sb.WriteString(strconv.Itoa(h.toLine))
  276. } else {
  277. sb.WriteString(strconv.Itoa(h.toLine))
  278. sb.WriteByte(',')
  279. sb.WriteString(strconv.Itoa(h.toCount))
  280. }
  281. sb.WriteString(" @@")
  282. sb.WriteString(color.Reset(Frag))
  283. if h.ctxPrefix != "" {
  284. sb.WriteByte(' ')
  285. sb.WriteString(color[Func])
  286. sb.WriteString(h.ctxPrefix)
  287. sb.WriteString(color.Reset(Func))
  288. }
  289. sb.WriteByte('\n')
  290. for _, op := range h.ops {
  291. op.writeTo(sb, color)
  292. }
  293. }
  294. func (h *hunk) AddOp(t Operation, ss ...string) {
  295. n := len(ss)
  296. switch t {
  297. case Add:
  298. h.toCount += n
  299. case Delete:
  300. h.fromCount += n
  301. case Equal:
  302. h.toCount += n
  303. h.fromCount += n
  304. }
  305. for _, s := range ss {
  306. h.ops = append(h.ops, &op{s, t})
  307. }
  308. }
  309. type op struct {
  310. text string
  311. t Operation
  312. }
  313. func (o *op) writeTo(sb *strings.Builder, color ColorConfig) {
  314. colorKey := operationColorKey[o.t]
  315. sb.WriteString(color[colorKey])
  316. sb.WriteByte(operationChar[o.t])
  317. if strings.HasSuffix(o.text, "\n") {
  318. sb.WriteString(strings.TrimSuffix(o.text, "\n"))
  319. } else {
  320. sb.WriteString(o.text + "\n\\ No newline at end of file")
  321. }
  322. sb.WriteString(color.Reset(colorKey))
  323. sb.WriteByte('\n')
  324. }