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