dyph 0.6.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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
data/lib/dyph/outcome.rb
ADDED
@@ -0,0 +1,27 @@
|
|
1
|
+
module Dyph
|
2
|
+
class Outcome::Conflicted < Outcome
|
3
|
+
attr_reader :left, :right, :base
|
4
|
+
def initialize(left:, base:, right:)
|
5
|
+
@left = left
|
6
|
+
@base = base
|
7
|
+
@right = right
|
8
|
+
end
|
9
|
+
|
10
|
+
def ==(other)
|
11
|
+
self.class == other.class &&
|
12
|
+
self.left == other.left &&
|
13
|
+
self.base == other.base &&
|
14
|
+
self.right == other.right
|
15
|
+
end
|
16
|
+
|
17
|
+
alias_method :eql?, :==
|
18
|
+
|
19
|
+
def hash
|
20
|
+
self.left.hash ^ self.base.hash ^ self.right.hash
|
21
|
+
end
|
22
|
+
|
23
|
+
def apply(fun)
|
24
|
+
self.class.new(left: fun[@left], base: fun[@base], right: fun[@right])
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dyph
|
2
|
+
class Outcome::Resolved < Outcome
|
3
|
+
attr_reader :result
|
4
|
+
def initialize(result)
|
5
|
+
@result = result
|
6
|
+
@combiner = ->(x, y) { x + y }
|
7
|
+
end
|
8
|
+
|
9
|
+
def set_combiner(lambda)
|
10
|
+
@combiner = lambda
|
11
|
+
end
|
12
|
+
|
13
|
+
def ==(other)
|
14
|
+
self.class == other.class &&
|
15
|
+
self.result == other.result
|
16
|
+
end
|
17
|
+
|
18
|
+
alias_method :eql?, :==
|
19
|
+
|
20
|
+
def hash
|
21
|
+
self.result.hash
|
22
|
+
end
|
23
|
+
|
24
|
+
def combine(other)
|
25
|
+
@result = @combiner[@result, other.result]
|
26
|
+
self
|
27
|
+
end
|
28
|
+
|
29
|
+
def apply(fun)
|
30
|
+
Outcome::Resolved.new(fun[result])
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
module Dyph
|
2
|
+
module Support
|
3
|
+
module AssignAction
|
4
|
+
extend self
|
5
|
+
def self.get_action(lo_a:, lo_b:, hi_a:, hi_b:)
|
6
|
+
if lo_a <= hi_a && lo_b <= hi_b # for this change, the bounds are both 'normal'. the beginning of the change is before the end.
|
7
|
+
[:change, lo_a + 1, hi_a + 1, lo_b + 1, hi_b + 1]
|
8
|
+
elsif lo_a <= hi_a
|
9
|
+
[:delete, lo_a + 1, hi_a + 1, lo_b + 1, lo_b]
|
10
|
+
elsif lo_b <= hi_b
|
11
|
+
[:add, lo_a + 1, lo_a, lo_b + 1, hi_b + 1]
|
12
|
+
else
|
13
|
+
nil
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
module Dyph
|
2
|
+
module Support
|
3
|
+
module Collater
|
4
|
+
extend self
|
5
|
+
def collate_merge(merge_result, join_function, conflict_handler)
|
6
|
+
if merge_result.empty?
|
7
|
+
Dyph::MergeResult.new([Outcome::Resolved.new([])], join_function)
|
8
|
+
else
|
9
|
+
merge_result = combine_non_conflicts(merge_result)
|
10
|
+
if (merge_result.length == 1 && merge_result.first.resolved?)
|
11
|
+
Dyph::MergeResult.new(merge_result, join_function)
|
12
|
+
else
|
13
|
+
Dyph::MergeResult.new(merge_result, join_function, conflict: true, conflict_handler: conflict_handler)
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
private
|
19
|
+
# @param [in] results
|
20
|
+
# @return the list of conflicts with contiguous parts merged if they are non_conflicts
|
21
|
+
def combine_non_conflicts(results)
|
22
|
+
results.reduce([]) do |rs, r|
|
23
|
+
if rs.any? && rs.last.resolved? && r.resolved?
|
24
|
+
rs.last.combine(r)
|
25
|
+
else
|
26
|
+
rs << r
|
27
|
+
end
|
28
|
+
rs
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
module Dyph
|
2
|
+
module Support
|
3
|
+
|
4
|
+
class Diff3
|
5
|
+
def self.execute_diff(left, base, right, diff2)
|
6
|
+
Diff3.new(left, base, right, diff2).get_differences
|
7
|
+
end
|
8
|
+
|
9
|
+
def initialize(left, base, right, diff2)
|
10
|
+
@left = left
|
11
|
+
@right = right
|
12
|
+
@base = base
|
13
|
+
@diff2 = diff2
|
14
|
+
end
|
15
|
+
|
16
|
+
def get_differences
|
17
|
+
#[[action, base_lo, base_hi, side_lo, side_hi]...]
|
18
|
+
left_diff = @diff2.diff(@base, @left).map { |r| Diff2Command.new(*r) }
|
19
|
+
right_diff = @diff2.diff(@base, @right).map { |r| Diff2Command.new(*r) }
|
20
|
+
collapse_differences(DiffDoubleQueue.new(left_diff, right_diff))
|
21
|
+
end
|
22
|
+
|
23
|
+
Diff2Command = Struct.new(:code, :base_lo, :base_hi, :side_lo, :side_hi)
|
24
|
+
|
25
|
+
private
|
26
|
+
|
27
|
+
def collapse_differences(diffs_queue, differences=[])
|
28
|
+
if diffs_queue.finished?
|
29
|
+
differences
|
30
|
+
else
|
31
|
+
result_queue = DiffDoubleQueue.new
|
32
|
+
init_side = diffs_queue.choose_side
|
33
|
+
top_diff = diffs_queue.dequeue
|
34
|
+
|
35
|
+
result_queue.enqueue(init_side, top_diff)
|
36
|
+
|
37
|
+
diffs_queue.switch_sides
|
38
|
+
build_result_queue(diffs_queue, top_diff.base_hi, result_queue)
|
39
|
+
|
40
|
+
differences << determine_differnce(result_queue, init_side, diffs_queue.switch_sides)
|
41
|
+
collapse_differences(diffs_queue, differences)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
def build_result_queue(diffs_queue, prev_base_hi, result_queue)
|
46
|
+
#current side can be :left or :right
|
47
|
+
if queue_finished?(diffs_queue.peek, prev_base_hi)
|
48
|
+
result_queue
|
49
|
+
else
|
50
|
+
top_diff = diffs_queue.dequeue
|
51
|
+
result_queue.enqueue(diffs_queue.current_side, top_diff)
|
52
|
+
|
53
|
+
if prev_base_hi < top_diff.base_hi
|
54
|
+
#switch the current side and adjust the base_hi
|
55
|
+
diffs_queue.switch_sides
|
56
|
+
build_result_queue(diffs_queue, top_diff.base_hi, result_queue)
|
57
|
+
else
|
58
|
+
build_result_queue(diffs_queue, prev_base_hi, result_queue)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
def queue_finished?(queue, prev_base_hi)
|
64
|
+
queue.empty? || queue.first.base_lo > prev_base_hi + 1
|
65
|
+
end
|
66
|
+
|
67
|
+
def determine_differnce(diff_diffs_queue, init_side, final_side)
|
68
|
+
base_lo = diff_diffs_queue.get(init_side).first.base_lo
|
69
|
+
base_hi = diff_diffs_queue.get(final_side).last.base_hi
|
70
|
+
# puts "Beta base_lo #{base_lo} base_hi #{base_hi}"
|
71
|
+
left_lo, left_hi = diffable_endpoints(diff_diffs_queue.get(:left), base_lo, base_hi)
|
72
|
+
right_lo, right_hi = diffable_endpoints(diff_diffs_queue.get(:right), base_lo, base_hi)
|
73
|
+
|
74
|
+
#the endpoints are offset one, neet to account for that in getting subsets
|
75
|
+
left_subset = @left[left_lo-1 .. left_hi]
|
76
|
+
right_subset = @right[right_lo-1 .. right_hi]
|
77
|
+
change_type = decide_action(diff_diffs_queue, left_subset, right_subset)
|
78
|
+
[change_type, left_lo, left_hi, right_lo, right_hi, base_lo, base_hi]
|
79
|
+
end
|
80
|
+
|
81
|
+
def diffable_endpoints(command, base_lo, base_hi)
|
82
|
+
if command.any?
|
83
|
+
lo = command.first.side_lo - command.first.base_lo + base_lo
|
84
|
+
hi = command.last.side_hi - command.last.base_hi + base_hi
|
85
|
+
[lo, hi]
|
86
|
+
else
|
87
|
+
[base_lo, base_hi]
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def decide_action(diff_diffs_queue, left_subset, right_subset)
|
92
|
+
#adjust because the ranges are 1 indexed
|
93
|
+
|
94
|
+
if diff_diffs_queue.empty?(:left)
|
95
|
+
:choose_right
|
96
|
+
elsif diff_diffs_queue.empty?(:right)
|
97
|
+
:choose_left
|
98
|
+
else
|
99
|
+
if left_subset != right_subset
|
100
|
+
:possible_conflict
|
101
|
+
else
|
102
|
+
:no_conflict_found
|
103
|
+
end
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
class DiffDoubleQueue
|
109
|
+
attr_reader :current_side
|
110
|
+
def initialize(left=[], right=[])
|
111
|
+
@diffs = { left: left, right: right }
|
112
|
+
end
|
113
|
+
|
114
|
+
def dequeue(side=current_side)
|
115
|
+
@diffs[side].shift
|
116
|
+
end
|
117
|
+
|
118
|
+
def peek(side=current_side)
|
119
|
+
@diffs[side]
|
120
|
+
end
|
121
|
+
|
122
|
+
def finished?
|
123
|
+
empty?(:left) && empty?(:right)
|
124
|
+
end
|
125
|
+
|
126
|
+
def enqueue(side=current_side, val)
|
127
|
+
@diffs[side] << val
|
128
|
+
end
|
129
|
+
|
130
|
+
def get(side=current_side)
|
131
|
+
@diffs[side]
|
132
|
+
end
|
133
|
+
|
134
|
+
def empty?(side=current_side)
|
135
|
+
@diffs[side].empty?
|
136
|
+
end
|
137
|
+
|
138
|
+
def switch_sides(side=current_side)
|
139
|
+
@current_side = side == :left ? :right : :left
|
140
|
+
end
|
141
|
+
|
142
|
+
def choose_side
|
143
|
+
if empty? :left
|
144
|
+
@current_side = :right
|
145
|
+
elsif empty? :right
|
146
|
+
@current_side = :left
|
147
|
+
else
|
148
|
+
#choose the lowest side relative to base
|
149
|
+
@current_side = get(:left).first.base_lo <= get(:right).first.base_lo ? :left : :right
|
150
|
+
end
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,169 @@
|
|
1
|
+
module Dyph
|
2
|
+
module Support
|
3
|
+
class Merger
|
4
|
+
attr_reader :result, :diff2
|
5
|
+
def self.merge(left, base, right, diff2: Dyph::Differ.default_diff2, diff3: Dyph::Differ.default_diff3)
|
6
|
+
merger = Merger.new(left: left, base: base, right: right, diff2: diff2, diff3: diff3)
|
7
|
+
merger.execute_three_way_merge()
|
8
|
+
merger.result
|
9
|
+
end
|
10
|
+
|
11
|
+
def initialize(left:, base:, right:, diff2:, diff3:)
|
12
|
+
@result = []
|
13
|
+
@diff2 = diff2
|
14
|
+
@diff3 = diff3
|
15
|
+
@text3 = Text3.new(left: left, right: right, base: base)
|
16
|
+
end
|
17
|
+
|
18
|
+
# rubocop:disable Metrics/AbcSize
|
19
|
+
def execute_three_way_merge
|
20
|
+
d3 = @diff3.execute_diff(@text3.left, @text3.base, @text3.right, @diff2)
|
21
|
+
chunk_descs = d3.map { |raw_chunk_desc| ChunkDesc.new(raw_chunk_desc) }
|
22
|
+
index = 1
|
23
|
+
chunk_descs.each do |chunk_desc|
|
24
|
+
initial_text = []
|
25
|
+
|
26
|
+
(index ... chunk_desc.base_lo).each do |lineno| # exclusive (...)
|
27
|
+
initial_text << @text3.base[lineno - 1]
|
28
|
+
end
|
29
|
+
#initial_text = initial_text.join("\n") + "\n"
|
30
|
+
#
|
31
|
+
@result << Dyph::Outcome::Resolved.new(initial_text) unless initial_text.empty?
|
32
|
+
|
33
|
+
interpret_chunk(chunk_desc)
|
34
|
+
#assign index to be the line in base after the conflict
|
35
|
+
index = chunk_desc.base_hi + 1
|
36
|
+
#
|
37
|
+
end
|
38
|
+
|
39
|
+
#finish by putting all text after the last conflict into the @result body.
|
40
|
+
|
41
|
+
ending_text = accumulate_lines(index, @text3.base.length, @text3.base)
|
42
|
+
|
43
|
+
@result << Dyph::Outcome::Resolved.new(ending_text) unless ending_text.empty?
|
44
|
+
end
|
45
|
+
# rubocop:enable Metrics/AbcSize
|
46
|
+
|
47
|
+
protected
|
48
|
+
def set_conflict(chunk_desc)
|
49
|
+
conflict = Dyph::Outcome::Conflicted.new(
|
50
|
+
left: accumulate_lines(chunk_desc.left_lo, chunk_desc.left_hi, @text3.left),
|
51
|
+
base: accumulate_lines(chunk_desc.base_lo, chunk_desc.base_hi, @text3.base),
|
52
|
+
right: accumulate_lines(chunk_desc.right_lo, chunk_desc.right_hi, @text3.right)
|
53
|
+
)
|
54
|
+
@result << conflict
|
55
|
+
end
|
56
|
+
|
57
|
+
def determine_conflict(d, left, right)
|
58
|
+
ia = 1
|
59
|
+
d.each do |raw_chunk_desc|
|
60
|
+
chunk_desc = ChunkDesc.new(raw_chunk_desc)
|
61
|
+
(ia ... chunk_desc.left_lo).each do |lineno|
|
62
|
+
@result << Dyph::Outcome::Resolved.new(accumulate_lines(ia, lineno, right))
|
63
|
+
end
|
64
|
+
|
65
|
+
outcome = determine_outcome(chunk_desc, left, right)
|
66
|
+
ia = chunk_desc.right_hi + 1
|
67
|
+
@result << outcome if outcome
|
68
|
+
end
|
69
|
+
|
70
|
+
final_text = accumulate_lines(ia, right.length + 1, right)
|
71
|
+
@result << Dyph::Outcome::Resolved.new(final_text) unless final_text.empty?
|
72
|
+
end
|
73
|
+
|
74
|
+
def determine_outcome(chunk_desc, left, right)
|
75
|
+
if chunk_desc.action == :change
|
76
|
+
Outcome::Conflicted.new(
|
77
|
+
left: accumulate_lines(chunk_desc.right_lo, chunk_desc.right_hi, left),
|
78
|
+
right: accumulate_lines(chunk_desc.left_lo, chunk_desc.left_hi, right),
|
79
|
+
base: []
|
80
|
+
)
|
81
|
+
elsif chunk_desc.action == :add
|
82
|
+
Outcome::Resolved.new(
|
83
|
+
accumulate_lines(chunk_desc.right_lo, chunk_desc.right_hi, left)
|
84
|
+
)
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
88
|
+
def set_text(orig_text, lo, hi)
|
89
|
+
text = [] # conflicting lines in right
|
90
|
+
(lo .. hi).each do |i| # inclusive(..)
|
91
|
+
text << orig_text[i - 1]
|
92
|
+
end
|
93
|
+
text
|
94
|
+
end
|
95
|
+
|
96
|
+
def _conflict_range(chunk_desc)
|
97
|
+
right = set_text(@text3.right, chunk_desc.right_lo, chunk_desc.right_hi)
|
98
|
+
left = set_text(@text3.left , chunk_desc.left_lo, chunk_desc.left_hi)
|
99
|
+
d = @diff2.diff(right, left)
|
100
|
+
if (_assoc_range(d, :change) || _assoc_range(d, :delete)) && chunk_desc.base_lo <= chunk_desc.base_hi
|
101
|
+
set_conflict(chunk_desc)
|
102
|
+
else
|
103
|
+
determine_conflict(d, left, right)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
|
107
|
+
def interpret_chunk(chunk_desc)
|
108
|
+
if chunk_desc.action == :choose_left
|
109
|
+
# 0 flag means choose left. put lines chunk_desc[1] .. chunk_desc[2] into the @result body.
|
110
|
+
temp_text = accumulate_lines(chunk_desc.left_lo, chunk_desc.left_hi, @text3.left)
|
111
|
+
# they deleted it, don't use if its only a new line
|
112
|
+
@result << Dyph::Outcome::Resolved.new(temp_text) unless temp_text.empty?
|
113
|
+
elsif chunk_desc.action != :possible_conflict
|
114
|
+
# A flag means choose right. put lines chunk_desc[3] to chunk_desc[4] into the @result body.
|
115
|
+
temp_text = accumulate_lines(chunk_desc.right_lo, chunk_desc.right_hi, @text3.right)
|
116
|
+
@result << Dyph::Outcome::Resolved.new(temp_text) unless temp_text.empty?
|
117
|
+
else
|
118
|
+
_conflict_range(chunk_desc)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
|
122
|
+
# @param [in] diff conflicts in diff structure
|
123
|
+
# @param [in] diff_type type of diff looked for in diff
|
124
|
+
# @return diff_type if any conflicts in diff are of type diff_type. otherwise return nil
|
125
|
+
def _assoc_range(diff, diff_type)
|
126
|
+
diff.each do |d|
|
127
|
+
if d[0] == diff_type
|
128
|
+
return d
|
129
|
+
end
|
130
|
+
end
|
131
|
+
nil
|
132
|
+
end
|
133
|
+
|
134
|
+
# @param [in] lo indec for beginning of accumulation range
|
135
|
+
# @param [in] hi index for end of accumulation range
|
136
|
+
# @param [in] text array of lines of text
|
137
|
+
# @return a string of lines lo to high joined by new lines, with a trailing new line.
|
138
|
+
def accumulate_lines(lo, hi, text)
|
139
|
+
lines = []
|
140
|
+
(lo .. hi).each do |lineno|
|
141
|
+
lines << text[lineno - 1] unless text[lineno - 1].nil?
|
142
|
+
end
|
143
|
+
lines
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
class Text3
|
148
|
+
attr_reader :left, :right, :base
|
149
|
+
def initialize(left:, right:, base:)
|
150
|
+
@left = left
|
151
|
+
@right = right
|
152
|
+
@base = base
|
153
|
+
end
|
154
|
+
end
|
155
|
+
|
156
|
+
class ChunkDesc
|
157
|
+
attr_reader :action, :left_lo, :left_hi, :right_lo, :right_hi, :base_lo, :base_hi
|
158
|
+
def initialize(raw_chunk)
|
159
|
+
@action = raw_chunk[0]
|
160
|
+
@left_lo = raw_chunk[1]
|
161
|
+
@left_hi = raw_chunk[2]
|
162
|
+
@right_lo = raw_chunk[3]
|
163
|
+
@right_hi = raw_chunk[4]
|
164
|
+
@base_lo = raw_chunk[5]
|
165
|
+
@base_hi = raw_chunk[6]
|
166
|
+
end
|
167
|
+
end
|
168
|
+
end
|
169
|
+
end
|