Class: PhraseAppUpdater::Differ
- Inherits:
-
Object
- Object
- PhraseAppUpdater::Differ
- Defined in:
- lib/phraseapp_updater/differ.rb
Constant Summary collapse
- SEPARATOR =
'~~~'
Instance Method Summary collapse
- #apply_diffs(hash, diffs) ⇒ Object
-
#initialize(verbose: false) ⇒ Differ
constructor
A new instance of Differ.
- #resolve!(original:, primary:, secondary:) ⇒ Object
-
#resolve_diffs(primary:, secondary:, secondary_deleted_prefixes:) ⇒ Object
Resolution strategy is that primary always wins in the event of a conflict.
-
#restore_deletions(current, previous) ⇒ Object
Prefer everything in current except deletions, which are restored from previous if available.
Constructor Details
#initialize(verbose: false) ⇒ Differ
Returns a new instance of Differ.
12 13 14 |
# File 'lib/phraseapp_updater/differ.rb', line 12 def initialize(verbose: false) @verbose = verbose end |
Instance Method Details
#apply_diffs(hash, diffs) ⇒ Object
65 66 67 |
# File 'lib/phraseapp_updater/differ.rb', line 65 def apply_diffs(hash, diffs) deep_compact!(Hashdiff.patch!(hash, diffs)) end |
#resolve!(original:, primary:, secondary:) ⇒ Object
69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 |
# File 'lib/phraseapp_updater/differ.rb', line 69 def resolve!(original:, primary:, secondary:) # To appropriately cope with type changes on either sides, flatten the # trees before calculating the difference and then expand afterwards. f_original = flatten(original) f_primary = flatten(primary) f_secondary = flatten(secondary) primary_diffs = Hashdiff.diff(f_original, f_primary) secondary_diffs = Hashdiff.diff(f_original, f_secondary) # However, flattening discards one critical piece of information: when we # have deleted or clobbered an entire prefix (subtree) from the original, # we want to consider this deletion atomic. If any of the changes is # cancelled, they must all be. Motivating example: # # original: { word: { one: "..", "many": ".." } } # primary: { word: { one: "..", "many": "..", "zero": ".." } } # secondary: { word: ".." } # would unexpectedly result in { word: { zero: ".." } }. # # Additionally calculate subtree prefixes that were deleted in `secondary`: secondary_deleted_prefixes = Hashdiff.diff(original, secondary, delimiter: SEPARATOR).lazy .select { |op, path, from, to| (op == "-" || op == "~") && from.is_a?(Hash) && !to.is_a?(Hash) } .map { |op, path, from, to| path } .to_a resolved_diffs = resolve_diffs(primary: primary_diffs, secondary: secondary_diffs, secondary_deleted_prefixes: secondary_deleted_prefixes) if @verbose STDERR.puts('Primary diffs:') primary_diffs.each { |d| STDERR.puts(d.inspect) } STDERR.puts('Secondary diffs:') secondary_diffs.each { |d| STDERR.puts(d.inspect) } STDERR.puts('Resolution:') resolved_diffs.each { |d| STDERR.puts(d.inspect) } end Hashdiff.patch!(f_original, resolved_diffs) (f_original) end |
#resolve_diffs(primary:, secondary:, secondary_deleted_prefixes:) ⇒ Object
Resolution strategy is that primary always wins in the event of a conflict
17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 |
# File 'lib/phraseapp_updater/differ.rb', line 17 def resolve_diffs(primary:, secondary:, secondary_deleted_prefixes:) primary = primary.index_by { |op, path, from, to| path } secondary = secondary.index_by { |op, path, from, to| path } # As well as explicit conflicts, we want to make sure that deletions or # incompatible type changes to a `primary` key prevent addition of child # keys in `secondary`. Because input hashes are flattened, it's never # possible for a given path and its prefix to be in the same input. # For example, in: # # primary = [["+", "a", 1]] # secondary = [["+", "a.b", 2]] # # the secondary change is impossible to perform on top of the primary, and # must be blocked. # # This applies in reverse: prefixes of paths in `p` need to be available # as hashes, so must not appear as terminals in `s`: # # primary = [["+", "a.b", 2]] # secondary = [["+", "a", 1]] primary_prefixes = primary.keys.flat_map { |p| path_prefixes(p) }.to_set # Remove conflicting entries from secondary, recording incompatible # changes. path_conflicts = [] secondary.delete_if do |path, diff| if primary_prefixes.include?(path) || primary.keys.any? { |pk| path.start_with?(pk) } path_conflicts << path unless primary.has_key?(path) && diff == primary[path] true else false end end # For all path conflicts matching secondary_deleted_prefixes, additionally # remove other changes with the same prefix. prefix_conflicts = secondary_deleted_prefixes.select do |prefix| path_conflicts.any? { |path| path.start_with?(prefix) } end secondary.delete_if do |path, diff| prefix_conflicts.any? { |prefix| path.start_with?(prefix) } end primary.values + secondary.values end |
#restore_deletions(current, previous) ⇒ Object
Prefer everything in current except deletions, which are restored from previous if available
120 121 122 |
# File 'lib/phraseapp_updater/differ.rb', line 120 def restore_deletions(current, previous) current.deep_merge(previous) end |