mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-25 00:28:31 -05:00
2855d2a402
Made conflict finding a bit more liberal, in that any messing with arrays (adding, removing or moving things inside) will conflict with any other such change.
177 lines
6.7 KiB
CoffeeScript
177 lines
6.7 KiB
CoffeeScript
SystemNameLoader = require 'lib/SystemNameLoader'
|
|
###
|
|
Good-to-knows:
|
|
dataPath: an array of keys that walks you up a JSON object that's being patched
|
|
ex: ['scripts', 0, 'description']
|
|
deltaPath: an array of keys that walks you up a JSON Diff Patch object.
|
|
ex: ['scripts', '_0', 'description']
|
|
###
|
|
|
|
module.exports.expandDelta = (delta, left, schema) ->
|
|
flattenedDeltas = flattenDelta(delta)
|
|
(expandFlattenedDelta(fd, left, schema) for fd in flattenedDeltas)
|
|
|
|
|
|
flattenDelta = (delta, dataPath=null, deltaPath=null) ->
|
|
# takes a single jsondiffpatch delta and returns an array of objects with
|
|
return [] unless delta
|
|
dataPath ?= []
|
|
deltaPath ?= []
|
|
return [{dataPath:dataPath, deltaPath: deltaPath, o:delta}] if _.isArray delta
|
|
|
|
results = []
|
|
affectingArray = delta._t is 'a'
|
|
for deltaIndex, childDelta of delta
|
|
continue if deltaIndex is '_t'
|
|
dataIndex = if affectingArray then parseInt(deltaIndex.replace('_', '')) else deltaIndex
|
|
results = results.concat flattenDelta(
|
|
childDelta, dataPath.concat([dataIndex]), deltaPath.concat([deltaIndex]))
|
|
results
|
|
|
|
|
|
expandFlattenedDelta = (delta, left, schema) ->
|
|
# takes a single flattened delta and converts into an object that can be
|
|
# easily formatted into something human readable.
|
|
|
|
delta.action = '???'
|
|
o = delta.o # the raw jsondiffpatch delta
|
|
|
|
if _.isArray(o) and o.length is 1
|
|
delta.action = 'added'
|
|
delta.newValue = o[0]
|
|
|
|
if _.isArray(o) and o.length is 2
|
|
delta.action = 'modified'
|
|
delta.oldValue = o[0]
|
|
delta.newValue = o[1]
|
|
|
|
if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 0
|
|
delta.action = 'deleted'
|
|
delta.oldValue = o[0]
|
|
|
|
if _.isPlainObject(o) and o._t is 'a'
|
|
delta.action = 'modified-array'
|
|
|
|
if _.isPlainObject(o) and o._t isnt 'a'
|
|
delta.action = 'modified-object'
|
|
|
|
if _.isArray(o) and o.length is 3 and o[2] is 3
|
|
delta.action = 'moved-index'
|
|
delta.destinationIndex = o[1]
|
|
delta.originalIndex = delta.dataPath[delta.dataPath.length-1]
|
|
|
|
if _.isArray(o) and o.length is 3 and o[1] is 0 and o[2] is 2
|
|
delta.action = 'text-diff'
|
|
delta.unidiff = o[0]
|
|
|
|
humanPath = []
|
|
parentLeft = left
|
|
parentSchema = schema
|
|
for key, i in delta.dataPath
|
|
# TODO: Better schema/json walking
|
|
childSchema = parentSchema?.items or parentSchema?.properties?[key] or {}
|
|
childLeft = parentLeft?[key]
|
|
humanKey = null
|
|
childData = if i is delta.dataPath.length-1 and delta.action is 'added' then o[0] else childLeft
|
|
humanKey ?= childData.name or childData.id if childData
|
|
humanKey ?= SystemNameLoader.getName(childData?.original)
|
|
humanKey ?= "#{childSchema.title}" if childSchema.title
|
|
humanKey ?= _.string.titleize key
|
|
humanPath.push humanKey
|
|
parentLeft = childLeft
|
|
parentSchema = childSchema
|
|
|
|
delta.humanPath = humanPath.join(' :: ')
|
|
delta.schema = childSchema
|
|
delta.left = childLeft
|
|
delta.right = jsondiffpatch.patch childLeft, delta.o unless delta.action is 'moved-index'
|
|
|
|
delta
|
|
|
|
module.exports.makeJSONDiffer = ->
|
|
hasher = (obj) -> obj.name || obj.id || obj._id || JSON.stringify(_.keys(obj))
|
|
jsondiffpatch.create({objectHash:hasher})
|
|
|
|
module.exports.getConflicts = (headDeltas, pendingDeltas) ->
|
|
# headDeltas and pendingDeltas should be lists of deltas returned by expandDelta
|
|
# Returns a list of conflict objects with properties:
|
|
# headDelta
|
|
# pendingDelta
|
|
# The deltas that have conflicts also have conflict properties pointing to one another.
|
|
|
|
headPathMap = groupDeltasByAffectingPaths(headDeltas)
|
|
pendingPathMap = groupDeltasByAffectingPaths(pendingDeltas)
|
|
paths = _.keys(headPathMap).concat(_.keys(pendingPathMap))
|
|
|
|
# Here's my thinking: conflicts happen when one delta path is a substring of another delta path
|
|
# So, sort paths from both deltas together, which will naturally make conflicts adjacent,
|
|
# and if one is identified AND one path is from the headDeltas AND the other is from pendingDeltas
|
|
# This is all to avoid an O(nm) brute force search.
|
|
|
|
conflicts = []
|
|
paths.sort()
|
|
for path, i in paths
|
|
offset = 1
|
|
while i + offset < paths.length
|
|
# Look at the neighbor
|
|
nextPath = paths[i+offset]
|
|
offset += 1
|
|
|
|
# these stop being substrings of each other? Then conflict DNE
|
|
if not (nextPath.startsWith path) then break
|
|
|
|
# check if these two are from the same group, but we still need to check for more beyond
|
|
unless headPathMap[path] or headPathMap[nextPath] then continue
|
|
unless pendingPathMap[path] or pendingPathMap[nextPath] then continue
|
|
|
|
# Okay, we found two deltas from different groups which conflict
|
|
for headMetaDelta in (headPathMap[path] or headPathMap[nextPath])
|
|
headDelta = headMetaDelta.delta
|
|
for pendingMetaDelta in (pendingPathMap[path] or pendingPathMap[nextPath])
|
|
pendingDelta = pendingMetaDelta.delta
|
|
conflicts.push({headDelta:headDelta, pendingDelta:pendingDelta})
|
|
pendingDelta.conflict = headDelta
|
|
headDelta.conflict = pendingDelta
|
|
|
|
return conflicts if conflicts.length
|
|
|
|
groupDeltasByAffectingPaths = (deltas) ->
|
|
metaDeltas = []
|
|
for delta in deltas
|
|
conflictPaths = []
|
|
# We're being fairly liberal with what's a conflict, because the alternative is worse
|
|
if delta.action is 'moved-index'
|
|
# If you moved items around in an array, mark the whole array as a gonner
|
|
conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1)
|
|
else if delta.action in ['deleted', 'added'] and _.isNumber(delta.dataPath[delta.dataPath.length-1])
|
|
# If you remove or add items in an array, mark the whole thing as a gonner
|
|
conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1)
|
|
else
|
|
conflictPaths.push delta.dataPath
|
|
for path in conflictPaths
|
|
metaDeltas.push {
|
|
delta: delta
|
|
path: (item.toString() for item in path).join('/')
|
|
}
|
|
|
|
map = _.groupBy metaDeltas, 'path'
|
|
return map
|
|
|
|
module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
|
|
expandedDeltas = (conflict.pendingDelta for conflict in conflicts)
|
|
module.exports.pruneExpandedDeltasFromDelta delta, expandedDeltas
|
|
|
|
module.exports.pruneExpandedDeltasFromDelta = (delta, expandedDeltas) ->
|
|
# the jsondiffpatch delta mustn't include any dangling nodes,
|
|
# or else things will get removed which shouldn't be, or errors will occur
|
|
for expandedDelta in expandedDeltas
|
|
prunePath delta, expandedDelta.deltaPath
|
|
if _.isEmpty delta then undefined else delta
|
|
|
|
prunePath = (delta, path) ->
|
|
if path.length is 1
|
|
delete delta[path]
|
|
else
|
|
prunePath delta[path[0]], path.slice(1)
|
|
keys = (k for k in _.keys(delta[path[0]]) when k isnt '_t')
|
|
delete delta[path[0]] if keys.length is 0
|