parse-diff.js 4.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192
  1. // From https://github.com/sergeyt/parse-diff
  2. module.exports = function (input) {
  3. if (!input) { return [] }
  4. if (input.match(/^\s+$/)) { return [] }
  5. const lines = input.split('\n')
  6. if (lines.length === 0) { return [] }
  7. const files = []
  8. let file = null
  9. let lnDel = 0
  10. let lnAdd = 0
  11. let current = null
  12. const start = function (line) {
  13. file = {
  14. chunks: [],
  15. deletions: 0,
  16. additions: 0
  17. }
  18. files.push(file)
  19. if (!file.to && !file.from) {
  20. const fileNames = parseFile(line)
  21. if (fileNames) {
  22. file.from = fileNames[0]
  23. file.to = fileNames[1]
  24. }
  25. }
  26. }
  27. const restart = function () {
  28. if (!file || file.chunks.length) { return start() }
  29. }
  30. const newFile = function () {
  31. restart()
  32. file.new = true
  33. file.from = '/dev/null'
  34. }
  35. const deletedFile = function () {
  36. restart()
  37. file.deleted = true
  38. file.to = '/dev/null'
  39. }
  40. const index = function (line) {
  41. restart()
  42. file.index = line.split(' ').slice(1)
  43. }
  44. const fromFile = function (line) {
  45. restart()
  46. file.from = parseFileFallback(line)
  47. }
  48. const toFile = function (line) {
  49. restart()
  50. file.to = parseFileFallback(line)
  51. }
  52. const binary = function (line) {
  53. file.binary = true
  54. }
  55. const chunk = function (line, match) {
  56. let newStart, oldStart
  57. lnDel = (oldStart = +match[1])
  58. const oldLines = +(match[2] || 0)
  59. lnAdd = (newStart = +match[3])
  60. const newLines = +(match[4] || 0)
  61. current = {
  62. content: line,
  63. changes: [],
  64. oldStart,
  65. oldLines,
  66. newStart,
  67. newLines
  68. }
  69. file.chunks.push(current)
  70. }
  71. const del = function (line) {
  72. if (!current) return
  73. current.changes.push({ type: 'del', del: true, ln: lnDel++, content: line })
  74. file.deletions++
  75. }
  76. const add = function (line) {
  77. if (!current) return
  78. current.changes.push({ type: 'add', add: true, ln: lnAdd++, content: line })
  79. file.additions++
  80. }
  81. const normal = function (line) {
  82. if (!current) return
  83. current.changes.push({
  84. type: 'normal',
  85. normal: true,
  86. ln1: lnDel++,
  87. ln2: lnAdd++,
  88. content: line
  89. })
  90. }
  91. const eof = function (line) {
  92. const recentChange = current.changes[current.changes.length - 1]
  93. return current.changes.push({
  94. type: recentChange.type,
  95. [recentChange.type]: true,
  96. ln1: recentChange.ln1,
  97. ln2: recentChange.ln2,
  98. ln: recentChange.ln,
  99. content: line
  100. })
  101. }
  102. const schema = [
  103. // todo beter regexp to avoid detect normal line starting with diff
  104. [/^\s+/, normal],
  105. [/^diff\s/, start],
  106. [/^new file mode \d+$/, newFile],
  107. [/^deleted file mode \d+$/, deletedFile],
  108. [/^Binary files/, binary],
  109. [/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
  110. [/^---\s/, fromFile],
  111. [/^\+\+\+\s/, toFile],
  112. [/^@@\s+-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
  113. [/^-/, del],
  114. [/^\+/, add],
  115. [/^\\ No newline at end of file$/, eof]
  116. ]
  117. const parse = function (line) {
  118. for (const p of schema) {
  119. const m = line.match(p[0])
  120. if (m) {
  121. p[1](line, m)
  122. return true
  123. }
  124. }
  125. return false
  126. }
  127. for (const line of lines) {
  128. parse(line)
  129. }
  130. return files
  131. }
  132. function parseFile (s) {
  133. if (!s) return
  134. const result = /\sa\/(.*)\sb\/(.*)/.exec(s)
  135. return [result[1], result[2]]
  136. }
  137. // fallback function to overwrite file.from and file.to if executed
  138. function parseFileFallback (s) {
  139. s = ltrim(s, '-')
  140. s = ltrim(s, '+')
  141. s = s.trim()
  142. // ignore possible time stamp
  143. const t = (/\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/).exec(s)
  144. if (t) { s = s.substring(0, t.index).trim() }
  145. // ignore git prefixes a/ or b/
  146. if (s.match((/^(a|b)\//))) { return s.substr(2) } else { return s }
  147. }
  148. function ltrim (s, chars) {
  149. s = makeString(s)
  150. if (!chars && trimLeft) { return trimLeft.call(s) }
  151. chars = defaultToWhiteSpace(chars)
  152. return s.replace(new RegExp(`^${chars}+`), '')
  153. }
  154. const makeString = s => s === null ? '' : s + ''
  155. const { trimLeft } = String.prototype
  156. function defaultToWhiteSpace (chars) {
  157. if (chars === null) { return '\\s' }
  158. if (chars.source) { return chars.source }
  159. return `[${escapeRegExp(chars)}]`
  160. }
  161. const escapeRegExp = s => makeString(s).replace(/([.*+?^=!:${}()|[\]/\\])/g, '\\$1')