parse-diff
Version:
Unified diff parser
164 lines (133 loc) • 3.58 kB
text/coffeescript
# parses unified diff
# http://www.gnu.org/software/diffutils/manual/diffutils.html#Unified-Format
module.exports = (input) ->
return [] if not input
return [] if input.match /^\s+$/
lines = input.split '\n'
return [] if lines.length == 0
files = []
file = null
ln_del = 0
ln_add = 0
current = null
start = (line) ->
file =
chunks: []
deletions: 0
additions: 0
files.push file
if not file.to and not file.from
fileNames = parseFile line
if fileNames
file.from = fileNames[0]
file.to = fileNames[1]
restart = ->
start() if not file || file.chunks.length
new_file = ->
restart()
file.new = true
file.from = '/dev/null'
deleted_file = ->
restart()
file.deleted = true
file.to = '/dev/null'
index = (line) ->
restart()
file.index = line.split(' ').slice(1)
from_file = (line) ->
restart()
file.from = parseFileFallback line
to_file = (line) ->
restart()
file.to = parseFileFallback line
chunk = (line, match) ->
ln_del = oldStart = +match[1]
oldLines = +(match[2] || 0)
ln_add = newStart = +match[3]
newLines = +(match[4] || 0)
current = {
content: line,
changes: [],
oldStart, oldLines, newStart, newLines
}
file.chunks.push current
del = (line) ->
return unless current
current.changes.push {type:'del', del:true, ln:ln_del++, content:line}
file.deletions++
add = (line) ->
return unless current
current.changes.push {type:'add', add:true, ln:ln_add++, content:line}
file.additions++
normal = (line) ->
return unless current
current.changes.push {
type: 'normal'
normal: true
ln1: ln_del++
ln2: ln_add++
content: line
}
eof = (line) ->
[..., recentChange] = current.changes
current.changes.push {
type: recentChange.type
"#{recentChange.type}": true
ln1: recentChange.ln1
ln2: recentChange.ln2
ln: recentChange.ln
content: line
}
schema = [
# todo beter regexp to avoid detect normal line starting with diff
[/^\s+/, normal],
[/^diff\s/, start],
[/^new file mode \d+$/, new_file],
[/^deleted file mode \d+$/, deleted_file],
[/^index\s[\da-zA-Z]+\.\.[\da-zA-Z]+(\s(\d+))?$/, index],
[/^---\s/, from_file],
[/^\+\+\+\s/, to_file],
[/^@@\s+\-(\d+),?(\d+)?\s+\+(\d+),?(\d+)?\s@@/, chunk],
[/^-/, del],
[/^\+/, add],
[/^\\ No newline at end of file$/, eof]
]
parse = (line) ->
for p in schema
m = line.match p[0]
if m
p[1](line, m)
return true
return false
for line in lines
parse line
return files
parseFile = (s) ->
return if not s
fileNames = s.match(/a\/.*(?= b)|b\/.*$/g)
fileNames.map (fileName, i) ->
fileNames[i] = fileName.replace(/^(a|b)\//, '')
return fileNames
# fallback function to overwrite file.from and file.to if executed
parseFileFallback = (s) ->
s = ltrim s, '-'
s = ltrim s, '+'
s = s.trim()
# ignore possible time stamp
t = (/\t.*|\d{4}-\d\d-\d\d\s\d\d:\d\d:\d\d(.\d+)?\s(\+|-)\d\d\d\d/).exec(s)
s = s.substring(0, t.index).trim() if t
# ignore git prefixes a/ or b/
if s.match (/^(a|b)\//) then s.substr(2) else s
ltrim = (s, chars) ->
s = makeString(s)
return trimLeft.call(s) if !chars and trimLeft
chars = defaultToWhiteSpace(chars)
return s.replace(new RegExp('^' + chars + '+'), '')
makeString = (s) -> if s == null then '' else s + ''
trimLeft = String.prototype.trimLeft
defaultToWhiteSpace = (chars) ->
return '\\s' if chars == null
return chars.source if chars.source
return '[' + escapeRegExp(chars) + ']'
escapeRegExp = (s) ->
makeString(s).replace(/([.*+?^=!:${}()|[\]\/\\])/g, '\\$1')