bash-merge 2.0.6 → 7.1.1

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.
@@ -1,199 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module Bash
4
- module Merge
5
- # Resolves conflicts between template and destination Bash content
6
- # using structural signatures and configurable preferences.
7
- #
8
- # @example Basic usage
9
- # resolver = ConflictResolver.new(template_analysis, dest_analysis)
10
- # resolver.resolve(result)
11
- class ConflictResolver < ::Ast::Merge::ConflictResolverBase
12
- # Creates a new ConflictResolver
13
- #
14
- # @param template_analysis [FileAnalysis] Analyzed template file
15
- # @param dest_analysis [FileAnalysis] Analyzed destination file
16
- # @param preference [Symbol, Hash] Which version to prefer when
17
- # nodes have matching signatures:
18
- # - :destination (default) - Keep destination version (customizations)
19
- # - :template - Use template version (updates)
20
- # @param add_template_only_nodes [Boolean] Whether to add nodes only in template
21
- # @param match_refiner [#call, nil] Optional match refiner for fuzzy matching
22
- # @param options [Hash] Additional options for forward compatibility
23
- # @param node_typing [Hash{Symbol,String => #call}, nil] Node typing configuration
24
- # for per-node-type preferences
25
- def initialize(template_analysis, dest_analysis, preference: :destination, add_template_only_nodes: false, match_refiner: nil, node_typing: nil, **options)
26
- super(
27
- strategy: :batch,
28
- preference: preference,
29
- template_analysis: template_analysis,
30
- dest_analysis: dest_analysis,
31
- add_template_only_nodes: add_template_only_nodes,
32
- match_refiner: match_refiner,
33
- **options
34
- )
35
- @node_typing = node_typing
36
- @emitter = Emitter.new
37
- end
38
-
39
- # Resolve conflicts and populate the result
40
- #
41
- # @param result [MergeResult] Result object to populate
42
- def resolve(result)
43
- DebugLogger.time("ConflictResolver#resolve") do
44
- template_nodes = @template_analysis.nodes
45
- dest_nodes = @dest_analysis.nodes
46
-
47
- # Clear emitter for fresh merge
48
- @emitter.clear
49
-
50
- # Build signature maps
51
- template_by_sig = build_signature_map(template_nodes, @template_analysis)
52
- dest_by_sig = build_signature_map(dest_nodes, @dest_analysis)
53
-
54
- # Track which nodes have been processed
55
- processed_template_sigs = ::Set.new
56
- processed_dest_sigs = ::Set.new
57
-
58
- # Process nodes via emitter
59
- merge_nodes_to_emitter(
60
- template_nodes,
61
- dest_nodes,
62
- template_by_sig,
63
- dest_by_sig,
64
- processed_template_sigs,
65
- processed_dest_sigs,
66
- )
67
-
68
- # Transfer emitter output to result
69
- emitted_content = @emitter.to_s
70
- unless emitted_content.empty?
71
- emitted_content.lines.each do |line|
72
- result.add_line(line.chomp, decision: MergeResult::DECISION_MERGED, source: :merged)
73
- end
74
- end
75
-
76
- DebugLogger.debug("Conflict resolution complete", {
77
- template_nodes: template_nodes.size,
78
- dest_nodes: dest_nodes.size,
79
- result_lines: result.line_count,
80
- })
81
- end
82
- end
83
-
84
- private
85
-
86
- def merge_nodes_to_emitter(template_nodes, dest_nodes, template_by_sig, dest_by_sig, processed_template_sigs, processed_dest_sigs)
87
- # First pass: Process destination nodes and find matches
88
- dest_nodes.each do |dest_node|
89
- dest_sig = @dest_analysis.generate_signature(dest_node)
90
-
91
- # Freeze blocks from destination are always preserved
92
- if freeze_node?(dest_node)
93
- emit_freeze_block(dest_node)
94
- processed_dest_sigs << dest_sig if dest_sig
95
- next
96
- end
97
-
98
- if dest_sig && template_by_sig[dest_sig]
99
- # Found matching node in template
100
- template_info = template_by_sig[dest_sig].first
101
- template_node = template_info[:node]
102
-
103
- # Decide which to emit based on preference
104
- emit_preferred_node(template_node, dest_node)
105
-
106
- processed_dest_sigs << dest_sig
107
- processed_template_sigs << dest_sig
108
- else
109
- # Destination-only node - always keep
110
- emit_node(dest_node, @dest_analysis)
111
- processed_dest_sigs << dest_sig if dest_sig
112
- end
113
- end
114
-
115
- # Second pass: Add template-only nodes if configured
116
- return unless @add_template_only_nodes
117
-
118
- template_nodes.each do |template_node|
119
- template_sig = @template_analysis.generate_signature(template_node)
120
-
121
- # Skip if already processed (matched with dest)
122
- next if template_sig && processed_template_sigs.include?(template_sig)
123
-
124
- # Skip freeze blocks from template
125
- next if freeze_node?(template_node)
126
-
127
- # Add template-only node
128
- emit_node(template_node, @template_analysis)
129
- processed_template_sigs << template_sig if template_sig
130
- end
131
- end
132
-
133
- def emit_preferred_node(template_node, dest_node)
134
- if preference_for_pair(template_node, dest_node) == :destination
135
- emit_node(dest_node, @dest_analysis)
136
- else
137
- emit_node(template_node, @template_analysis)
138
- end
139
- end
140
-
141
- def preference_for_pair(template_node, dest_node)
142
- return @preference unless @preference.is_a?(Hash)
143
-
144
- typed_template = apply_node_typing(template_node)
145
- typed_dest = apply_node_typing(dest_node)
146
-
147
- if Ast::Merge::NodeTyping.typed_node?(typed_template)
148
- merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_template)
149
- return @preference.fetch(merge_type) { default_preference } if merge_type
150
- end
151
-
152
- if Ast::Merge::NodeTyping.typed_node?(typed_dest)
153
- merge_type = Ast::Merge::NodeTyping.merge_type_for(typed_dest)
154
- return @preference.fetch(merge_type) { default_preference } if merge_type
155
- end
156
-
157
- default_preference
158
- end
159
-
160
- def apply_node_typing(node)
161
- return node unless @node_typing
162
- return node unless node
163
-
164
- Ast::Merge::NodeTyping.process(node, @node_typing)
165
- end
166
-
167
- # Emit a single node to the emitter
168
- # @param node [NodeWrapper] Node to emit
169
- # @param analysis [FileAnalysis] Analysis for accessing source
170
- def emit_node(node, analysis)
171
- return if freeze_node?(node)
172
-
173
- # Emit leading comments
174
- if node.start_line
175
- leading = analysis.comment_tracker.leading_comments_before(node.start_line)
176
- leading.each do |comment|
177
- @emitter.emit_tracked_comment(comment)
178
- end
179
- end
180
-
181
- # Emit the node content
182
- if node.start_line && node.end_line
183
- lines = []
184
- (node.start_line..node.end_line).each do |line_num|
185
- line = analysis.line_at(line_num)
186
- lines << line if line
187
- end
188
- @emitter.emit_raw_lines(lines)
189
- end
190
- end
191
-
192
- # Emit a freeze block
193
- # @param freeze_node [FreezeNode] Freeze block to emit
194
- def emit_freeze_block(freeze_node)
195
- @emitter.emit_raw_lines(freeze_node.lines)
196
- end
197
- end
198
- end
199
- end