dyph 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,12 @@
1
+ module Dyph
2
+ class Outcome
3
+ def resolved?
4
+ self.class == Outcome::Resolved
5
+ end
6
+
7
+ def conflicted?
8
+ self.class == Outcome::Conflicted
9
+ end
10
+
11
+ end
12
+ end
@@ -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