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
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
|