mirror of
https://github.com/codeninjasllc/codecombat.git
synced 2024-11-27 09:35:39 -05:00
Made the deltas lib conflict identifying library able to handle many-to-many conflicts.
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.
This commit is contained in:
parent
cc917162a7
commit
2855d2a402
5 changed files with 53 additions and 36 deletions
|
@ -93,7 +93,7 @@ module.exports.makeJSONDiffer = ->
|
|||
jsondiffpatch.create({objectHash:hasher})
|
||||
|
||||
module.exports.getConflicts = (headDeltas, pendingDeltas) ->
|
||||
# headDeltas and pendingDeltas should be lists of deltas returned by interpretDelta
|
||||
# headDeltas and pendingDeltas should be lists of deltas returned by expandDelta
|
||||
# Returns a list of conflict objects with properties:
|
||||
# headDelta
|
||||
# pendingDelta
|
||||
|
@ -105,17 +105,30 @@ module.exports.getConflicts = (headDeltas, pendingDeltas) ->
|
|||
|
||||
# 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, one path is from the headDeltas, the other is from pendingDeltas
|
||||
# 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
|
||||
continue if i + 1 is paths.length
|
||||
nextPath = paths[i+1]
|
||||
if nextPath.startsWith path
|
||||
headDelta = (headPathMap[path] or headPathMap[nextPath])[0].delta
|
||||
pendingDelta = (pendingPathMap[path] or pendingPathMap[nextPath])[0].delta
|
||||
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
|
||||
|
@ -126,12 +139,13 @@ 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'
|
||||
# every other action affects just the data path, but moved indexes affect a swath
|
||||
indices = [delta.originalIndex, delta.destinationIndex]
|
||||
indices.sort()
|
||||
for index in _.range(indices[0], indices[1]+1)
|
||||
conflictPaths.push delta.dataPath.slice(0, delta.dataPath.length-1).concat(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
|
||||
|
@ -141,25 +155,7 @@ groupDeltasByAffectingPaths = (deltas) ->
|
|||
}
|
||||
|
||||
map = _.groupBy metaDeltas, 'path'
|
||||
|
||||
# Turns out there are cases where a single delta can include paths
|
||||
# that 'conflict' with each other, ie one is a substring of the other
|
||||
# because of moved indices. To handle this case, go through and prune
|
||||
# out all deeper paths that conflict with more shallow paths, so
|
||||
# getConflicts path checking works properly.
|
||||
|
||||
paths = _.keys(map)
|
||||
return map unless paths.length
|
||||
paths.sort()
|
||||
prunedMap = {}
|
||||
previousPath = paths[0]
|
||||
for path, i in paths
|
||||
continue if i is 0
|
||||
continue if path.startsWith previousPath
|
||||
prunedMap[path] = map[path]
|
||||
previousPath = path
|
||||
|
||||
prunedMap
|
||||
return map
|
||||
|
||||
module.exports.pruneConflictsFromDelta = (delta, conflicts) ->
|
||||
expandedDeltas = (conflict.pendingDelta for conflict in conflicts)
|
||||
|
|
|
@ -58,7 +58,9 @@ class CocoModel extends Backbone.Model
|
|||
@set(existing, {silent:true})
|
||||
CocoModel.backedUp[@id] = @
|
||||
|
||||
saveBackup: ->
|
||||
saveBackup: -> @saveBackupNow()
|
||||
|
||||
saveBackupNow: ->
|
||||
storage.save(@id, @attributes)
|
||||
CocoModel.backedUp[@id] = @
|
||||
|
||||
|
|
|
@ -73,7 +73,7 @@ module.exports = class EditorLevelView extends View
|
|||
Backbone.Mediator.publish 'level-loaded', level: @level
|
||||
@showReadOnly() if me.get('anonymous')
|
||||
@patchesView = @insertSubView(new PatchesView(@level), @$el.find('.patches-view'))
|
||||
@listenTo @patchesView, 'accepted-patch', -> setTimeout "location.reload()", 400
|
||||
@listenTo @patchesView, 'accepted-patch', -> location.reload()
|
||||
@$el.find('#level-watch-button').find('> span').toggleClass('secret') if @level.watching()
|
||||
|
||||
onPlayLevel: (e) ->
|
||||
|
|
|
@ -57,6 +57,7 @@ module.exports = class PatchModal extends ModalView
|
|||
acceptPatch: ->
|
||||
delta = @deltaView.getApplicableDelta()
|
||||
@targetModel.applyDelta(delta)
|
||||
@targetModel.saveBackupNow()
|
||||
@patch.setStatus('accepted')
|
||||
@trigger 'accepted-patch'
|
||||
@hide()
|
||||
|
|
18
test/app/lib/deltas.spec.coffee
Normal file
18
test/app/lib/deltas.spec.coffee
Normal file
|
@ -0,0 +1,18 @@
|
|||
deltas = require 'lib/deltas'
|
||||
|
||||
describe 'deltas lib', ->
|
||||
|
||||
describe 'getConflicts', ->
|
||||
|
||||
it 'handles conflicts where one change conflicts with several changes', ->
|
||||
originalData = {list:[1,2,3]}
|
||||
forkA = {list:['1', 2, '3']}
|
||||
forkB = {noList: '...'}
|
||||
differ = deltas.makeJSONDiffer()
|
||||
|
||||
expandedDeltaA = deltas.expandDelta(differ.diff originalData, forkA)
|
||||
expandedDeltaB = deltas.expandDelta(differ.diff originalData, forkB)
|
||||
deltas.getConflicts(expandedDeltaA, expandedDeltaB)
|
||||
for delta in expandedDeltaA
|
||||
expect(delta.conflict).toBeDefined()
|
||||
|
Loading…
Reference in a new issue