dyph 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,73 @@
1
+ module Dyph
2
+ module Support
3
+ module SanityCheck
4
+ extend self
5
+
6
+ # rubocop:disable Metrics/AbcSize
7
+ def ensure_no_lost_data(left, base, right, final_result)
8
+ result_word_map = {}
9
+ final_result.each do |result_block|
10
+ blocks = case result_block
11
+ when Outcome::Resolved then result_block.result
12
+ when Outcome::Conflicted then [result_block.left, result_block.right].flatten
13
+ else raise "Unknown block type, #{result_block[:type]}"
14
+ end
15
+ count_blocks(blocks, result_word_map)
16
+ end
17
+
18
+ left_word_map, base_word_map, right_word_map = [left, base, right].map { |str| count_blocks(str) }
19
+
20
+ # new words are words that are in left or right, but not in base
21
+ new_left_words = subtract_words(left_word_map, base_word_map)
22
+ new_right_words = subtract_words(right_word_map, base_word_map)
23
+
24
+ # now make sure all new words are somewhere in the result
25
+ missing_new_left_words = subtract_words(new_left_words, result_word_map)
26
+ missing_new_right_words = subtract_words(new_right_words, result_word_map)
27
+
28
+ if missing_new_left_words.any? || missing_new_right_words.any?
29
+ raise BadMergeException.new(final_result)
30
+ end
31
+ end
32
+ # rubocop:enable Metrics/AbcSize
33
+
34
+ private
35
+ def count_blocks(blocks, hash={})
36
+ blocks.reduce(hash) do |map, block|
37
+ map[block] ||= 0
38
+ map[block] += 1
39
+ map
40
+ end
41
+ end
42
+
43
+ def subtract_words(left_map, right_map)
44
+ remaining_words = {}
45
+
46
+ left_map.each do |word, count|
47
+ count_in_right = right_map[word] || 0
48
+
49
+ new_count = count - count_in_right
50
+ remaining_words[word] = new_count if new_count > 0
51
+ end
52
+
53
+ remaining_words
54
+ end
55
+ end
56
+
57
+ class BadMergeException < StandardError
58
+ attr_accessor :merge_result
59
+
60
+ def initialize(merge_result)
61
+ @merge_result = merge_result
62
+ end
63
+
64
+ def inspect
65
+ "<#{self.class}: #{merge_result}>"
66
+ end
67
+
68
+ def to_s
69
+ inspect
70
+ end
71
+ end
72
+ end
73
+ end
@@ -0,0 +1,190 @@
1
+ module Dyph
2
+ module TwoWayDiffers
3
+
4
+ class HeckelDiff
5
+ # Algorithm adapted from http://www.rad.upenn.edu/sbia/software/basis/apidoc/v1.2/diff3_8py_source.html
6
+
7
+ def self.execute_diff(old_text_array, new_text_array)
8
+ raise ArgumentError, "Argument is not an array." unless old_text_array.is_a?(Array) && new_text_array.is_a?(Array)
9
+ diff_result = diff(old_text_array, new_text_array)
10
+ # convert to typed differ's output (wrapped with change types eg. Add, Delete, Change)
11
+ HeckelDiffWrapper.new(old_text_array, new_text_array, diff_result).convert_to_typed_ouput
12
+ end
13
+
14
+ # Two-way diff based on the algorithm by P. Heckel.
15
+ # @param [in] left Array of anything implementing hash and equals
16
+ # @param [in] right Array of anything implementing hash and equals
17
+ def self.diff(left, right)
18
+ differ = HeckelDiff.new(left,right)
19
+ differ.perform_diff
20
+ end
21
+
22
+ def initialize(left, right)
23
+ @left = left
24
+ @right = right
25
+ end
26
+
27
+ def perform_diff
28
+ unique_positions = identify_unique_postions
29
+ unique_positions.sort!{ |a, b| a[0] <=> b[0] } # sort by the line in which the line was found in a
30
+ left_change_pos, right_change_pos = find_next_change
31
+ init_changes = ChangeData.new(left_change_pos, right_change_pos, [])
32
+ final_changes = unique_positions.reduce(init_changes, &method(:get_differences))
33
+ final_changes.change_ranges
34
+ end
35
+
36
+ ChangeData = Struct.new(:left_change_pos, :right_change_pos, :change_ranges)
37
+
38
+ private
39
+
40
+ def get_differences(change_data, unique_positions)
41
+ left_pos, right_pos = change_data.left_change_pos, change_data.right_change_pos
42
+ left_uniq_pos, right_uniq_pos = unique_positions
43
+ if left_uniq_pos < left_pos || right_uniq_pos < right_pos
44
+ change_data
45
+ else
46
+ left_lo, left_hi, right_lo, right_hi = find_prev_change(left_pos, right_pos, left_uniq_pos-1, right_uniq_pos-1)
47
+ next_left_pos, next_right_pos = find_next_change(left_uniq_pos+1, right_uniq_pos+1)
48
+
49
+ updated_ranges = append_change_range(change_data.change_ranges, left_lo, left_hi, right_lo, right_hi)
50
+ ChangeData.new(next_left_pos, next_right_pos, updated_ranges)
51
+ end
52
+ end
53
+
54
+ def find_next_change(left_start_pos=0, right_start_pos=0)
55
+ l_arr, r_arr = (@left[left_start_pos..-1] || []), (@right[right_start_pos..-1] || [])
56
+ offset = mismatch_offset l_arr, r_arr
57
+ [ left_start_pos + offset, right_start_pos + offset]
58
+ end
59
+
60
+
61
+ def find_prev_change(left_lo, right_lo, left_hi, right_hi)
62
+ if left_lo > left_hi || right_lo > right_hi
63
+ [left_lo, left_hi, right_lo, right_hi]
64
+ else
65
+ l_arr, r_arr = (@left[left_lo .. left_hi].reverse || []), (@right[right_lo .. right_hi].reverse || [])
66
+ offset = mismatch_offset l_arr, r_arr
67
+ [left_lo, left_hi - offset, right_lo, right_hi - offset]
68
+ end
69
+ end
70
+
71
+ def mismatch_offset(l_arr, r_arr)
72
+ _ , index = l_arr.zip(r_arr).each_with_index.detect { |pair, _| pair[0] != pair[1] }
73
+ index || [l_arr.length, r_arr.length].min
74
+ end
75
+
76
+ def identify_unique_postions
77
+ left_uniques = find_unique(@left)
78
+ right_uniques = find_unique(@right)
79
+ shared_keys = left_uniques.keys & right_uniques.keys
80
+ uniq_ranges = shared_keys.map { |k| [left_uniques[k], right_uniques[k]] }
81
+ uniq_ranges.unshift([ @left.length, @right.length])
82
+ end
83
+
84
+ def find_unique(array)
85
+ flagged_uniques = array.each_with_index.reduce({}) do |hash, item_index|
86
+ item, pos = item_index
87
+ hash[item] = {pos: pos, unique: hash[item].nil?}
88
+ hash
89
+ end
90
+ flagged_uniques.select { |_, v| v[:unique] }.map { |k, v| [k, v[:pos]] }.to_h
91
+ end
92
+
93
+ # given the calculated bounds of the 2 way diff, create the proper change type and add it to the queue.
94
+ def append_change_range(changes_ranges, left_lo, left_hi, right_lo, right_hi)
95
+ if left_lo <= left_hi && right_lo <= right_hi # for this change, the bounds are both 'normal'. the beginning of the change is before the end.
96
+ changes_ranges << [:change, left_lo + 1, left_hi + 1, right_lo + 1, right_hi + 1]
97
+ elsif left_lo <= left_hi
98
+ changes_ranges << [:delete, left_lo + 1, left_hi + 1, right_lo + 1, right_lo]
99
+ elsif right_lo <= right_hi
100
+ changes_ranges << [:add, left_lo + 1, left_lo, right_lo + 1, right_hi + 1]
101
+ end
102
+ changes_ranges
103
+ end
104
+ end
105
+
106
+ class TextNode
107
+ attr_accessor :text, :row
108
+
109
+ def initialize(text:, row:)
110
+ @text = text
111
+ @row = row
112
+ end
113
+ end
114
+
115
+ class TwoWayChunk
116
+ attr_reader :action, :left_lo, :left_hi, :right_lo, :right_hi
117
+ def initialize(raw_chunk)
118
+ @action = raw_chunk[0]
119
+ @left_lo = raw_chunk[1]
120
+ @left_hi = raw_chunk[2]
121
+ @right_lo = raw_chunk[3]
122
+ @right_hi = raw_chunk[4]
123
+ end
124
+ end
125
+ class HeckelDiffWrapper
126
+ def initialize(old_text_array, new_text_array, heckel_diff)
127
+ @chunks = heckel_diff.map { |block| TwoWayChunk.new(block) }
128
+ @old_text_array = old_text_array
129
+ @new_text_array = new_text_array
130
+ @old_text = []
131
+ @new_text = []
132
+ end
133
+ IndexTracker = Struct.new(:old_index, :new_index)
134
+
135
+ def convert_to_typed_ouput()
136
+ final_indexes = @chunks.reduce(IndexTracker.new(0,0)) do |index_tracker, chunk|
137
+ old_iteration, new_iteration = set_text_node_indexes(chunk, index_tracker.old_index, index_tracker.new_index)
138
+ old_index, new_index = append_changes(chunk, index_tracker.old_index + old_iteration, index_tracker.new_index + new_iteration)
139
+ IndexTracker.new(old_index, new_index)
140
+ end
141
+
142
+ set_the_remaining_text_node_indexes(final_indexes.old_index, final_indexes.new_index)
143
+ { old_text: @old_text, new_text: @new_text}
144
+ end
145
+
146
+ private
147
+ def set_text_node_indexes(chunk, old_index, new_index)
148
+ old_iteration = 0
149
+ while old_index + old_iteration < chunk.left_lo - 1 # chunk indexes are from 1
150
+ @old_text << TextNode.new(text: @old_text_array[old_index + old_iteration], row: new_index + old_iteration)
151
+ old_iteration += 1
152
+ end
153
+
154
+ new_iteration = 0
155
+ while new_index + new_iteration < chunk.right_lo - 1 # chunk indexes are from 1
156
+ @new_text << TextNode.new(text: @new_text_array[new_index + new_iteration], row: old_index + new_iteration)
157
+ new_iteration += 1
158
+ end
159
+ [old_iteration, new_iteration]
160
+ end
161
+
162
+ def append_changes(chunk, old_index, new_index)
163
+ while old_index <= chunk.left_hi - 1 # chunk indexes are from 1
164
+ @old_text << @old_text_array[old_index]
165
+ old_index += 1
166
+ end
167
+
168
+ while new_index <= chunk.right_hi - 1 # chunk indexes are from 1
169
+ @new_text << @new_text_array[new_index]
170
+ new_index += 1
171
+ end
172
+ [old_index, new_index]
173
+ end
174
+
175
+ def set_the_remaining_text_node_indexes(old_index, new_index)
176
+ iteration = 0
177
+ while old_index + iteration < @old_text_array.length
178
+ @old_text << TextNode.new(text: @old_text_array[old_index + iteration], row: new_index + iteration)
179
+ iteration += 1
180
+ end
181
+
182
+ iteration = 0
183
+ while new_index + iteration < @new_text_array.length
184
+ @new_text << TextNode.new(text: @new_text_array[new_index + iteration], row: old_index + iteration)
185
+ iteration += 1
186
+ end
187
+ end
188
+ end
189
+ end
190
+ end
@@ -0,0 +1,179 @@
1
+ module Dyph
2
+ module TwoWayDiffers
3
+ # rubocop:disable Metrics/ModuleLength
4
+ module OutputConverter
5
+ extend self
6
+
7
+ def convert_to_dyph_output(old_text, new_text)
8
+ actions = merge_and_partition(old_text, new_text)
9
+ selected_actions = extract_add_deletes_changes(actions)
10
+ correct_offsets(selected_actions)
11
+ end
12
+
13
+ def objectify(merge_results)
14
+ merge_results.map do |result|
15
+ action = result[:action]
16
+ line = result[:line]
17
+ old_index = result[:old_index]
18
+ new_index = result[:new_index]
19
+
20
+ case action
21
+ when :add
22
+ Dyph::Action::Add.new(value: line, old_index: old_index, new_index: new_index)
23
+ when :delete
24
+ Dyph::Action::Delete.new(value: line, old_index: old_index, new_index: new_index)
25
+ when :no_change
26
+ Dyph::Action::NoChange.new(value: line.text, old_index: old_index, new_index: new_index)
27
+ else
28
+ raise "unhandled action"
29
+ end
30
+ end
31
+ end
32
+
33
+ def merge_results(old_text, new_text)
34
+ merged_text = []
35
+
36
+ if (new_text.empty?)
37
+ no_new_text(old_text, merged_text)
38
+ else
39
+ prepend_old_text(old_text, merged_text)
40
+ gather_up_actions(old_text, new_text, merged_text)
41
+ end
42
+ merged_text
43
+ end
44
+
45
+ private
46
+ def merge_and_partition(old_text, new_text)
47
+ merged_text = merge_results(old_text, new_text)
48
+ merge_output_lines = merged_text.map { |x| to_output_format x }
49
+ partition_into_actions(merge_output_lines)
50
+ end
51
+
52
+ def extract_add_deletes_changes(actions)
53
+ collapsed_actions = actions.map { |action| collapse_action(action) }
54
+ paired_results = pair_up_add_deletes collapsed_actions
55
+ paired_results.reject { |res| res[:action] == :no_change}
56
+ end
57
+
58
+ def correct_offsets(selected_actions)
59
+ fix_offsets = set_offset(selected_actions)
60
+ fix_offsets.map { |r| Dyph::Support::AssignAction.get_action(
61
+ lo_a: r[:old_lo]-1 ,
62
+ lo_b: r[:new_lo]-1,
63
+ hi_a: r[:old_hi]-1,
64
+ hi_b: r[:new_hi]-1
65
+ )}
66
+ end
67
+
68
+ def gather_up_actions(old_text, new_text, merged_text)
69
+ prev_no_change_old = - 1
70
+ new_text.map.with_index.each do |line, i|
71
+ if !line.is_a?(TextNode)
72
+ merged_text << {action: :add, line: line, old_index: prev_no_change_old + 1, new_index: i+1}
73
+ else
74
+ prev_no_change_old = line.row
75
+ change_or_delete(old_text, line, prev_no_change_old, merged_text, i)
76
+ end
77
+ end
78
+ end
79
+
80
+ def change_or_delete(old_text, line, prev_no_change_old, merged_text, index)
81
+ merged_text << {action: :no_change, line: line, old_index: line.row, new_index: index}
82
+
83
+ ((prev_no_change_old+1) ... old_text.length).each do |n|
84
+ break if old_text[n].is_a?(TextNode)
85
+ merged_text << {action: :delete, line: old_text[n], old_index: n+1, new_index: index+1}
86
+ end
87
+ end
88
+
89
+ def prepend_old_text(old_text, merged_text)
90
+ if !old_text.first.is_a?(TextNode)
91
+ old_text.map.with_index.each do |line, i|
92
+ break if line.is_a?(TextNode)
93
+ merged_text << {action: :delete, line: line, old_index:i+1, new_index: i}
94
+ end
95
+ end
96
+ end
97
+
98
+ def no_new_text(old_text, merged_text)
99
+ old_text.map.with_index do |line, i|
100
+ merged_text << { action: :delete, line: line, old_index: i+1, new_index: i}
101
+ end
102
+ end
103
+
104
+ def set_offset(results)
105
+ results.map do |result_row|
106
+ if result_row[:action] == :add
107
+ result_row[:old_lo] += 1
108
+ result_row
109
+ elsif result_row[:action] == :delete
110
+ result_row[:new_lo] += 1
111
+ result_row
112
+ else
113
+ result_row
114
+ end
115
+ end
116
+ end
117
+
118
+ def to_output_format(result_row)
119
+ {
120
+ action: result_row[:action],
121
+ old_lo: result_row[:old_index],
122
+ old_hi: result_row[:old_index],
123
+ new_lo: result_row[:new_index],
124
+ new_hi: result_row[:new_index]
125
+ }
126
+ end
127
+
128
+ def collapse_action(actions)
129
+ actions.inject({}) do | hash, action |
130
+ hash[:action] ||= action[:action]
131
+ hash[:old_lo] ||= action[:old_lo]
132
+ hash[:old_hi] = action[:old_hi]
133
+ hash[:new_lo] ||= action[:new_lo]
134
+ hash[:new_hi] = action[:new_hi]
135
+ hash
136
+ end
137
+ end
138
+
139
+ def is_a_pair?(actions, i)
140
+ action_one = actions[i-1][:action] if actions[i-1]
141
+ action_two = actions[i][:action] if actions[i]
142
+ Set.new([action_one, action_two]) == Set.new([:add, :delete])
143
+ end
144
+
145
+ def pair_up_add_deletes(actions)
146
+ results = []
147
+ found_change = false
148
+ (1 .. actions.length).each do |i|
149
+ if is_a_pair?(actions, i)
150
+ results << {
151
+ action: :change,
152
+ old_lo: actions[i-1][:old_lo],
153
+ old_hi: actions[i-1][:old_hi],
154
+ new_lo: actions[i][:new_lo],
155
+ new_hi: actions[i][:new_hi]
156
+ }
157
+ found_change = true
158
+ elsif found_change
159
+ found_change = false
160
+ else
161
+ results << actions[i-1]
162
+ end
163
+ end
164
+ results
165
+ end
166
+
167
+ def partition_into_actions(array)
168
+ array.inject([]) do |acc, x|
169
+ if acc.length == 0 || acc.last.last[:action] != x[:action]
170
+ acc << [x]
171
+ else
172
+ acc.last << x
173
+ end
174
+ acc
175
+ end
176
+ end
177
+ end
178
+ end
179
+ end
@@ -0,0 +1,3 @@
1
+ module Dyph
2
+ VERSION = "0.6.0"
3
+ end
metadata ADDED
@@ -0,0 +1,220 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: dyph
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.6.0
5
+ platform: ruby
6
+ authors:
7
+ - Kevin Mook
8
+ - Andrew Montalto
9
+ - Jacob Elder
10
+ autorequire:
11
+ bindir: bin
12
+ cert_chain: []
13
+ date: 2020-09-28 00:00:00.000000000 Z
14
+ dependencies:
15
+ - !ruby/object:Gem::Dependency
16
+ name: bundler
17
+ requirement: !ruby/object:Gem::Requirement
18
+ requirements:
19
+ - - "~>"
20
+ - !ruby/object:Gem::Version
21
+ version: '2.1'
22
+ type: :development
23
+ prerelease: false
24
+ version_requirements: !ruby/object:Gem::Requirement
25
+ requirements:
26
+ - - "~>"
27
+ - !ruby/object:Gem::Version
28
+ version: '2.1'
29
+ - !ruby/object:Gem::Dependency
30
+ name: rake
31
+ requirement: !ruby/object:Gem::Requirement
32
+ requirements:
33
+ - - ">="
34
+ - !ruby/object:Gem::Version
35
+ version: '0'
36
+ type: :development
37
+ prerelease: false
38
+ version_requirements: !ruby/object:Gem::Requirement
39
+ requirements:
40
+ - - ">="
41
+ - !ruby/object:Gem::Version
42
+ version: '0'
43
+ - !ruby/object:Gem::Dependency
44
+ name: pry
45
+ requirement: !ruby/object:Gem::Requirement
46
+ requirements:
47
+ - - ">="
48
+ - !ruby/object:Gem::Version
49
+ version: '0'
50
+ type: :development
51
+ prerelease: false
52
+ version_requirements: !ruby/object:Gem::Requirement
53
+ requirements:
54
+ - - ">="
55
+ - !ruby/object:Gem::Version
56
+ version: '0'
57
+ - !ruby/object:Gem::Dependency
58
+ name: pry-rescue
59
+ requirement: !ruby/object:Gem::Requirement
60
+ requirements:
61
+ - - ">="
62
+ - !ruby/object:Gem::Version
63
+ version: '0'
64
+ type: :development
65
+ prerelease: false
66
+ version_requirements: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ - !ruby/object:Gem::Dependency
72
+ name: pry-stack_explorer
73
+ requirement: !ruby/object:Gem::Requirement
74
+ requirements:
75
+ - - ">="
76
+ - !ruby/object:Gem::Version
77
+ version: '0'
78
+ type: :development
79
+ prerelease: false
80
+ version_requirements: !ruby/object:Gem::Requirement
81
+ requirements:
82
+ - - ">="
83
+ - !ruby/object:Gem::Version
84
+ version: '0'
85
+ - !ruby/object:Gem::Dependency
86
+ name: rspec
87
+ requirement: !ruby/object:Gem::Requirement
88
+ requirements:
89
+ - - "~>"
90
+ - !ruby/object:Gem::Version
91
+ version: 3.3.0
92
+ type: :development
93
+ prerelease: false
94
+ version_requirements: !ruby/object:Gem::Requirement
95
+ requirements:
96
+ - - "~>"
97
+ - !ruby/object:Gem::Version
98
+ version: 3.3.0
99
+ - !ruby/object:Gem::Dependency
100
+ name: awesome_print
101
+ requirement: !ruby/object:Gem::Requirement
102
+ requirements:
103
+ - - ">="
104
+ - !ruby/object:Gem::Version
105
+ version: '0'
106
+ type: :development
107
+ prerelease: false
108
+ version_requirements: !ruby/object:Gem::Requirement
109
+ requirements:
110
+ - - ">="
111
+ - !ruby/object:Gem::Version
112
+ version: '0'
113
+ - !ruby/object:Gem::Dependency
114
+ name: factory_girl
115
+ requirement: !ruby/object:Gem::Requirement
116
+ requirements:
117
+ - - ">="
118
+ - !ruby/object:Gem::Version
119
+ version: '0'
120
+ type: :development
121
+ prerelease: false
122
+ version_requirements: !ruby/object:Gem::Requirement
123
+ requirements:
124
+ - - ">="
125
+ - !ruby/object:Gem::Version
126
+ version: '0'
127
+ - !ruby/object:Gem::Dependency
128
+ name: rspec_junit_formatter
129
+ requirement: !ruby/object:Gem::Requirement
130
+ requirements:
131
+ - - ">="
132
+ - !ruby/object:Gem::Version
133
+ version: '0'
134
+ type: :development
135
+ prerelease: false
136
+ version_requirements: !ruby/object:Gem::Requirement
137
+ requirements:
138
+ - - ">="
139
+ - !ruby/object:Gem::Version
140
+ version: '0'
141
+ - !ruby/object:Gem::Dependency
142
+ name: faker
143
+ requirement: !ruby/object:Gem::Requirement
144
+ requirements:
145
+ - - ">="
146
+ - !ruby/object:Gem::Version
147
+ version: '0'
148
+ type: :development
149
+ prerelease: false
150
+ version_requirements: !ruby/object:Gem::Requirement
151
+ requirements:
152
+ - - ">="
153
+ - !ruby/object:Gem::Version
154
+ version: '0'
155
+ - !ruby/object:Gem::Dependency
156
+ name: yard
157
+ requirement: !ruby/object:Gem::Requirement
158
+ requirements:
159
+ - - ">="
160
+ - !ruby/object:Gem::Version
161
+ version: '0'
162
+ type: :development
163
+ prerelease: false
164
+ version_requirements: !ruby/object:Gem::Requirement
165
+ requirements:
166
+ - - ">="
167
+ - !ruby/object:Gem::Version
168
+ version: '0'
169
+ description: A library of useful diffing algorithms for Ruby
170
+ email:
171
+ - kevin@kevinmook.com
172
+ executables: []
173
+ extensions: []
174
+ extra_rdoc_files: []
175
+ files:
176
+ - LICENSE
177
+ - README.md
178
+ - lib/dyph.rb
179
+ - lib/dyph/action.rb
180
+ - lib/dyph/action/add.rb
181
+ - lib/dyph/action/delete.rb
182
+ - lib/dyph/action/no_change.rb
183
+ - lib/dyph/differ.rb
184
+ - lib/dyph/equatable.rb
185
+ - lib/dyph/merge_result.rb
186
+ - lib/dyph/outcome.rb
187
+ - lib/dyph/outcome/conflicted.rb
188
+ - lib/dyph/outcome/resolved.rb
189
+ - lib/dyph/support/assign_action.rb
190
+ - lib/dyph/support/collater.rb
191
+ - lib/dyph/support/diff3.rb
192
+ - lib/dyph/support/merger.rb
193
+ - lib/dyph/support/sanity_check.rb
194
+ - lib/dyph/two_way_differs/heckel_diff.rb
195
+ - lib/dyph/two_way_differs/output_converter.rb
196
+ - lib/dyph/version.rb
197
+ homepage: https://github.com/kevinmookorg/dyph
198
+ licenses:
199
+ - MIT
200
+ metadata: {}
201
+ post_install_message:
202
+ rdoc_options: []
203
+ require_paths:
204
+ - lib
205
+ required_ruby_version: !ruby/object:Gem::Requirement
206
+ requirements:
207
+ - - ">="
208
+ - !ruby/object:Gem::Version
209
+ version: 2.6.0
210
+ required_rubygems_version: !ruby/object:Gem::Requirement
211
+ requirements:
212
+ - - ">="
213
+ - !ruby/object:Gem::Version
214
+ version: '0'
215
+ requirements: []
216
+ rubygems_version: 3.0.3
217
+ signing_key:
218
+ specification_version: 4
219
+ summary: A library of useful diffing algorithms for Ruby
220
+ test_files: []