dyph 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/LICENSE +21 -0
- data/README.md +199 -0
- data/lib/dyph.rb +25 -0
- data/lib/dyph/action.rb +12 -0
- data/lib/dyph/action/add.rb +7 -0
- data/lib/dyph/action/delete.rb +7 -0
- data/lib/dyph/action/no_change.rb +7 -0
- data/lib/dyph/differ.rb +102 -0
- data/lib/dyph/equatable.rb +24 -0
- data/lib/dyph/merge_result.rb +45 -0
- data/lib/dyph/outcome.rb +12 -0
- data/lib/dyph/outcome/conflicted.rb +27 -0
- data/lib/dyph/outcome/resolved.rb +33 -0
- data/lib/dyph/support/assign_action.rb +18 -0
- data/lib/dyph/support/collater.rb +33 -0
- data/lib/dyph/support/diff3.rb +155 -0
- data/lib/dyph/support/merger.rb +169 -0
- data/lib/dyph/support/sanity_check.rb +73 -0
- data/lib/dyph/two_way_differs/heckel_diff.rb +190 -0
- data/lib/dyph/two_way_differs/output_converter.rb +179 -0
- data/lib/dyph/version.rb +3 -0
- metadata +220 -0
@@ -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
|
data/lib/dyph/version.rb
ADDED
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: []
|