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 7.8KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367
  1. package diff
  2. import (
  3. "bytes"
  4. "fmt"
  5. "io"
  6. "regexp"
  7. "strings"
  8. "github.com/go-git/go-git/v5/plumbing"
  9. )
  10. const (
  11. diffInit = "diff --git a/%s b/%s\n"
  12. chunkStart = "@@ -"
  13. chunkMiddle = " +"
  14. chunkEnd = " @@%s\n"
  15. chunkCount = "%d,%d"
  16. noFilePath = "/dev/null"
  17. aDir = "a/"
  18. bDir = "b/"
  19. fPath = "--- %s\n"
  20. tPath = "+++ %s\n"
  21. binary = "Binary files %s and %s differ\n"
  22. addLine = "+%s%s"
  23. deleteLine = "-%s%s"
  24. equalLine = " %s%s"
  25. noNewLine = "\n\\ No newline at end of file\n"
  26. oldMode = "old mode %o\n"
  27. newMode = "new mode %o\n"
  28. deletedFileMode = "deleted file mode %o\n"
  29. newFileMode = "new file mode %o\n"
  30. renameFrom = "from"
  31. renameTo = "to"
  32. renameFileMode = "rename %s %s\n"
  33. indexAndMode = "index %s..%s %o\n"
  34. indexNoMode = "index %s..%s\n"
  35. DefaultContextLines = 3
  36. )
  37. // UnifiedEncoder encodes an unified diff into the provided Writer.
  38. // There are some unsupported features:
  39. // - Similarity index for renames
  40. // - Sort hash representation
  41. type UnifiedEncoder struct {
  42. io.Writer
  43. // ctxLines is the count of unchanged lines that will appear
  44. // surrounding a change.
  45. ctxLines int
  46. buf bytes.Buffer
  47. }
  48. func NewUnifiedEncoder(w io.Writer, ctxLines int) *UnifiedEncoder {
  49. return &UnifiedEncoder{ctxLines: ctxLines, Writer: w}
  50. }
  51. func (e *UnifiedEncoder) Encode(patch Patch) error {
  52. e.printMessage(patch.Message())
  53. if err := e.encodeFilePatch(patch.FilePatches()); err != nil {
  54. return err
  55. }
  56. _, err := e.buf.WriteTo(e)
  57. return err
  58. }
  59. func (e *UnifiedEncoder) encodeFilePatch(filePatches []FilePatch) error {
  60. for _, p := range filePatches {
  61. f, t := p.Files()
  62. if err := e.header(f, t, p.IsBinary()); err != nil {
  63. return err
  64. }
  65. g := newHunksGenerator(p.Chunks(), e.ctxLines)
  66. for _, c := range g.Generate() {
  67. c.WriteTo(&e.buf)
  68. }
  69. }
  70. return nil
  71. }
  72. func (e *UnifiedEncoder) printMessage(message string) {
  73. isEmpty := message == ""
  74. hasSuffix := strings.HasSuffix(message, "\n")
  75. if !isEmpty && !hasSuffix {
  76. message += "\n"
  77. }
  78. e.buf.WriteString(message)
  79. }
  80. func (e *UnifiedEncoder) header(from, to File, isBinary bool) error {
  81. switch {
  82. case from == nil && to == nil:
  83. return nil
  84. case from != nil && to != nil:
  85. hashEquals := from.Hash() == to.Hash()
  86. fmt.Fprintf(&e.buf, diffInit, from.Path(), to.Path())
  87. if from.Mode() != to.Mode() {
  88. fmt.Fprintf(&e.buf, oldMode+newMode, from.Mode(), to.Mode())
  89. }
  90. if from.Path() != to.Path() {
  91. fmt.Fprintf(&e.buf,
  92. renameFileMode+renameFileMode,
  93. renameFrom, from.Path(), renameTo, to.Path())
  94. }
  95. if from.Mode() != to.Mode() && !hashEquals {
  96. fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), to.Hash())
  97. } else if !hashEquals {
  98. fmt.Fprintf(&e.buf, indexAndMode, from.Hash(), to.Hash(), from.Mode())
  99. }
  100. if !hashEquals {
  101. e.pathLines(isBinary, aDir+from.Path(), bDir+to.Path())
  102. }
  103. case from == nil:
  104. fmt.Fprintf(&e.buf, diffInit, to.Path(), to.Path())
  105. fmt.Fprintf(&e.buf, newFileMode, to.Mode())
  106. fmt.Fprintf(&e.buf, indexNoMode, plumbing.ZeroHash, to.Hash())
  107. e.pathLines(isBinary, noFilePath, bDir+to.Path())
  108. case to == nil:
  109. fmt.Fprintf(&e.buf, diffInit, from.Path(), from.Path())
  110. fmt.Fprintf(&e.buf, deletedFileMode, from.Mode())
  111. fmt.Fprintf(&e.buf, indexNoMode, from.Hash(), plumbing.ZeroHash)
  112. e.pathLines(isBinary, aDir+from.Path(), noFilePath)
  113. }
  114. return nil
  115. }
  116. func (e *UnifiedEncoder) pathLines(isBinary bool, fromPath, toPath string) {
  117. format := fPath + tPath
  118. if isBinary {
  119. format = binary
  120. }
  121. fmt.Fprintf(&e.buf, format, fromPath, toPath)
  122. }
  123. type hunksGenerator struct {
  124. fromLine, toLine int
  125. ctxLines int
  126. chunks []Chunk
  127. current *hunk
  128. hunks []*hunk
  129. beforeContext, afterContext []string
  130. }
  131. func newHunksGenerator(chunks []Chunk, ctxLines int) *hunksGenerator {
  132. return &hunksGenerator{
  133. chunks: chunks,
  134. ctxLines: ctxLines,
  135. }
  136. }
  137. func (c *hunksGenerator) Generate() []*hunk {
  138. for i, chunk := range c.chunks {
  139. ls := splitLines(chunk.Content())
  140. lsLen := len(ls)
  141. switch chunk.Type() {
  142. case Equal:
  143. c.fromLine += lsLen
  144. c.toLine += lsLen
  145. c.processEqualsLines(ls, i)
  146. case Delete:
  147. if lsLen != 0 {
  148. c.fromLine++
  149. }
  150. c.processHunk(i, chunk.Type())
  151. c.fromLine += lsLen - 1
  152. c.current.AddOp(chunk.Type(), ls...)
  153. case Add:
  154. if lsLen != 0 {
  155. c.toLine++
  156. }
  157. c.processHunk(i, chunk.Type())
  158. c.toLine += lsLen - 1
  159. c.current.AddOp(chunk.Type(), ls...)
  160. }
  161. if i == len(c.chunks)-1 && c.current != nil {
  162. c.hunks = append(c.hunks, c.current)
  163. }
  164. }
  165. return c.hunks
  166. }
  167. func (c *hunksGenerator) processHunk(i int, op Operation) {
  168. if c.current != nil {
  169. return
  170. }
  171. var ctxPrefix string
  172. linesBefore := len(c.beforeContext)
  173. if linesBefore > c.ctxLines {
  174. ctxPrefix = " " + c.beforeContext[linesBefore-c.ctxLines-1]
  175. c.beforeContext = c.beforeContext[linesBefore-c.ctxLines:]
  176. linesBefore = c.ctxLines
  177. }
  178. c.current = &hunk{ctxPrefix: strings.TrimSuffix(ctxPrefix, "\n")}
  179. c.current.AddOp(Equal, c.beforeContext...)
  180. switch op {
  181. case Delete:
  182. c.current.fromLine, c.current.toLine =
  183. c.addLineNumbers(c.fromLine, c.toLine, linesBefore, i, Add)
  184. case Add:
  185. c.current.toLine, c.current.fromLine =
  186. c.addLineNumbers(c.toLine, c.fromLine, linesBefore, i, Delete)
  187. }
  188. c.beforeContext = nil
  189. }
  190. // addLineNumbers obtains the line numbers in a new chunk
  191. func (c *hunksGenerator) addLineNumbers(la, lb int, linesBefore int, i int, op Operation) (cla, clb int) {
  192. cla = la - linesBefore
  193. // we need to search for a reference for the next diff
  194. switch {
  195. case linesBefore != 0 && c.ctxLines != 0:
  196. if lb > c.ctxLines {
  197. clb = lb - c.ctxLines + 1
  198. } else {
  199. clb = 1
  200. }
  201. case c.ctxLines == 0:
  202. clb = lb
  203. case i != len(c.chunks)-1:
  204. next := c.chunks[i+1]
  205. if next.Type() == op || next.Type() == Equal {
  206. // this diff will be into this chunk
  207. clb = lb + 1
  208. }
  209. }
  210. return
  211. }
  212. func (c *hunksGenerator) processEqualsLines(ls []string, i int) {
  213. if c.current == nil {
  214. c.beforeContext = append(c.beforeContext, ls...)
  215. return
  216. }
  217. c.afterContext = append(c.afterContext, ls...)
  218. if len(c.afterContext) <= c.ctxLines*2 && i != len(c.chunks)-1 {
  219. c.current.AddOp(Equal, c.afterContext...)
  220. c.afterContext = nil
  221. } else {
  222. ctxLines := c.ctxLines
  223. if ctxLines > len(c.afterContext) {
  224. ctxLines = len(c.afterContext)
  225. }
  226. c.current.AddOp(Equal, c.afterContext[:ctxLines]...)
  227. c.hunks = append(c.hunks, c.current)
  228. c.current = nil
  229. c.beforeContext = c.afterContext[ctxLines:]
  230. c.afterContext = nil
  231. }
  232. }
  233. var splitLinesRE = regexp.MustCompile(`[^\n]*(\n|$)`)
  234. func splitLines(s string) []string {
  235. out := splitLinesRE.FindAllString(s, -1)
  236. if out[len(out)-1] == "" {
  237. out = out[:len(out)-1]
  238. }
  239. return out
  240. }
  241. type hunk struct {
  242. fromLine int
  243. toLine int
  244. fromCount int
  245. toCount int
  246. ctxPrefix string
  247. ops []*op
  248. }
  249. func (c *hunk) WriteTo(buf *bytes.Buffer) {
  250. buf.WriteString(chunkStart)
  251. if c.fromCount == 1 {
  252. fmt.Fprintf(buf, "%d", c.fromLine)
  253. } else {
  254. fmt.Fprintf(buf, chunkCount, c.fromLine, c.fromCount)
  255. }
  256. buf.WriteString(chunkMiddle)
  257. if c.toCount == 1 {
  258. fmt.Fprintf(buf, "%d", c.toLine)
  259. } else {
  260. fmt.Fprintf(buf, chunkCount, c.toLine, c.toCount)
  261. }
  262. fmt.Fprintf(buf, chunkEnd, c.ctxPrefix)
  263. for _, d := range c.ops {
  264. buf.WriteString(d.String())
  265. }
  266. }
  267. func (c *hunk) AddOp(t Operation, s ...string) {
  268. ls := len(s)
  269. switch t {
  270. case Add:
  271. c.toCount += ls
  272. case Delete:
  273. c.fromCount += ls
  274. case Equal:
  275. c.toCount += ls
  276. c.fromCount += ls
  277. }
  278. for _, l := range s {
  279. c.ops = append(c.ops, &op{l, t})
  280. }
  281. }
  282. type op struct {
  283. text string
  284. t Operation
  285. }
  286. func (o *op) String() string {
  287. var prefix, suffix string
  288. switch o.t {
  289. case Add:
  290. prefix = addLine
  291. case Delete:
  292. prefix = deleteLine
  293. case Equal:
  294. prefix = equalLine
  295. }
  296. n := len(o.text)
  297. if n > 0 && o.text[n-1] != '\n' {
  298. suffix = noNewLine
  299. }
  300. return fmt.Sprintf(prefix, o.text, suffix)
  301. }