bash-merge 2.0.6 → 7.1.3
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 +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +98 -645
- data/lib/bash/merge/comment_tracker.rb +36 -100
- data/lib/bash/merge/debug_logger.rb +10 -10
- data/lib/bash/merge/emitter.rb +45 -33
- data/lib/bash/merge/file_analysis.rb +133 -33
- data/lib/bash/merge/freeze_node.rb +9 -9
- data/lib/bash/merge/merge_result.rb +11 -5
- data/lib/bash/merge/node_wrapper.rb +89 -36
- data/lib/bash/merge/provider.rb +983 -0
- data/lib/bash/merge/smart_merger.rb +641 -32
- data/lib/bash/merge/version.rb +5 -4
- data/lib/bash/merge.rb +120 -25
- data/lib/bash-merge.rb +7 -1
- data/sig/bash/merge.rbs +18 -248
- data.tar.gz.sig +3 -1
- metadata +84 -88
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -211
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/lib/bash/merge/conflict_resolver.rb +0 -199
|
@@ -75,11 +75,11 @@ module Bash
|
|
|
75
75
|
# String representation for debugging
|
|
76
76
|
# @return [String]
|
|
77
77
|
def inspect
|
|
78
|
-
# :
|
|
78
|
+
# simplecov:disable
|
|
79
79
|
# Defensive: slice is always set via resolve_content in FreezeNodeBase#initialize
|
|
80
80
|
# The || 0 branch is normally unreachable because validate_structure! ensures non-empty content
|
|
81
81
|
content_length = slice&.length || 0
|
|
82
|
-
# :
|
|
82
|
+
# simplecov:enable
|
|
83
83
|
"#<#{self.class.name} lines=#{start_line}..#{end_line} content_length=#{content_length}>"
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -88,13 +88,13 @@ module Bash
|
|
|
88
88
|
def validate_structure!
|
|
89
89
|
validate_line_order!
|
|
90
90
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
91
|
+
return unless @lines.empty? || @lines.all?(&:nil?)
|
|
92
|
+
|
|
93
|
+
raise InvalidStructureError.new(
|
|
94
|
+
'Freeze block is empty',
|
|
95
|
+
start_line: @start_line,
|
|
96
|
+
end_line: @end_line
|
|
97
|
+
)
|
|
98
98
|
end
|
|
99
99
|
end
|
|
100
100
|
end
|
|
@@ -12,6 +12,8 @@ module Bash
|
|
|
12
12
|
# result.add_line("echo 'hello'", decision: :kept_template, source: :template)
|
|
13
13
|
# result.to_bash # => "echo 'hello'\n"
|
|
14
14
|
class MergeResult < Ast::Merge::MergeResultBase
|
|
15
|
+
include Ast::Merge::StructuredReviewApplySupport
|
|
16
|
+
|
|
15
17
|
# Inherit decision constants from base class
|
|
16
18
|
DECISION_KEPT_TEMPLATE = Ast::Merge::MergeResultBase::DECISION_KEPT_TEMPLATE
|
|
17
19
|
DECISION_KEPT_DEST = Ast::Merge::MergeResultBase::DECISION_KEPT_DEST
|
|
@@ -31,7 +33,7 @@ module Bash
|
|
|
31
33
|
dest_lines: 0,
|
|
32
34
|
merged_lines: 0,
|
|
33
35
|
freeze_preserved_lines: 0,
|
|
34
|
-
total_decisions: 0
|
|
36
|
+
total_decisions: 0
|
|
35
37
|
}
|
|
36
38
|
end
|
|
37
39
|
|
|
@@ -46,7 +48,7 @@ module Bash
|
|
|
46
48
|
content: line,
|
|
47
49
|
decision: decision,
|
|
48
50
|
source: source,
|
|
49
|
-
original_line: original_line
|
|
51
|
+
original_line: original_line
|
|
50
52
|
}
|
|
51
53
|
|
|
52
54
|
track_statistics(decision, source)
|
|
@@ -71,7 +73,7 @@ module Bash
|
|
|
71
73
|
# @param decision [Symbol] Decision for the blank line
|
|
72
74
|
# @param source [Symbol] Source
|
|
73
75
|
def add_blank_line(decision: DECISION_MERGED, source: :merged)
|
|
74
|
-
add_line(
|
|
76
|
+
add_line('', decision: decision, source: source)
|
|
75
77
|
end
|
|
76
78
|
|
|
77
79
|
# Add content from a freeze block
|
|
@@ -83,7 +85,7 @@ module Bash
|
|
|
83
85
|
line.chomp,
|
|
84
86
|
decision: DECISION_FREEZE_BLOCK,
|
|
85
87
|
source: :destination,
|
|
86
|
-
original_line: freeze_node.start_line + idx
|
|
88
|
+
original_line: freeze_node.start_line + idx
|
|
87
89
|
)
|
|
88
90
|
end
|
|
89
91
|
end
|
|
@@ -121,9 +123,13 @@ module Bash
|
|
|
121
123
|
to_bash
|
|
122
124
|
end
|
|
123
125
|
|
|
126
|
+
def to_s
|
|
127
|
+
to_bash
|
|
128
|
+
end
|
|
129
|
+
|
|
124
130
|
private
|
|
125
131
|
|
|
126
|
-
def track_statistics(decision,
|
|
132
|
+
def track_statistics(decision, _source)
|
|
127
133
|
@statistics[:total_decisions] += 1
|
|
128
134
|
|
|
129
135
|
case decision
|
|
@@ -19,22 +19,24 @@ module Bash
|
|
|
19
19
|
#
|
|
20
20
|
# @see Ast::Merge::NodeWrapperBase
|
|
21
21
|
class NodeWrapper < Ast::Merge::NodeWrapperBase
|
|
22
|
+
attr_reader :node
|
|
23
|
+
|
|
22
24
|
# Check if this is a function definition
|
|
23
25
|
# @return [Boolean]
|
|
24
26
|
def function_definition?
|
|
25
|
-
@node.type.to_s ==
|
|
27
|
+
@node.type.to_s == 'function_definition'
|
|
26
28
|
end
|
|
27
29
|
|
|
28
30
|
# Check if this is a variable assignment
|
|
29
31
|
# @return [Boolean]
|
|
30
32
|
def variable_assignment?
|
|
31
|
-
@node.type.to_s ==
|
|
33
|
+
@node.type.to_s == 'variable_assignment'
|
|
32
34
|
end
|
|
33
35
|
|
|
34
36
|
# Check if this is an if statement
|
|
35
37
|
# @return [Boolean]
|
|
36
38
|
def if_statement?
|
|
37
|
-
@node.type.to_s ==
|
|
39
|
+
@node.type.to_s == 'if_statement'
|
|
38
40
|
end
|
|
39
41
|
|
|
40
42
|
# Check if this is a for loop
|
|
@@ -46,31 +48,31 @@ module Bash
|
|
|
46
48
|
# Check if this is a while loop
|
|
47
49
|
# @return [Boolean]
|
|
48
50
|
def while_statement?
|
|
49
|
-
@node.type.to_s ==
|
|
51
|
+
@node.type.to_s == 'while_statement'
|
|
50
52
|
end
|
|
51
53
|
|
|
52
54
|
# Check if this is a case statement
|
|
53
55
|
# @return [Boolean]
|
|
54
56
|
def case_statement?
|
|
55
|
-
@node.type.to_s ==
|
|
57
|
+
@node.type.to_s == 'case_statement'
|
|
56
58
|
end
|
|
57
59
|
|
|
58
60
|
# Check if this is a command
|
|
59
61
|
# @return [Boolean]
|
|
60
62
|
def command?
|
|
61
|
-
@node.type.to_s ==
|
|
63
|
+
@node.type.to_s == 'command'
|
|
62
64
|
end
|
|
63
65
|
|
|
64
66
|
# Check if this is a pipeline
|
|
65
67
|
# @return [Boolean]
|
|
66
68
|
def pipeline?
|
|
67
|
-
@node.type.to_s ==
|
|
69
|
+
@node.type.to_s == 'pipeline'
|
|
68
70
|
end
|
|
69
71
|
|
|
70
72
|
# Check if this is a comment
|
|
71
73
|
# @return [Boolean]
|
|
72
74
|
def comment?
|
|
73
|
-
@node.type.to_s ==
|
|
75
|
+
@node.type.to_s == 'comment'
|
|
74
76
|
end
|
|
75
77
|
|
|
76
78
|
# Get the function name if this is a function definition
|
|
@@ -79,7 +81,7 @@ module Bash
|
|
|
79
81
|
return unless function_definition?
|
|
80
82
|
|
|
81
83
|
# In bash tree-sitter, function name is in a 'name' or 'word' child
|
|
82
|
-
name_node = find_child_by_type(
|
|
84
|
+
name_node = find_child_by_type('word') || find_child_by_field('name')
|
|
83
85
|
node_text(name_node) if name_node
|
|
84
86
|
end
|
|
85
87
|
|
|
@@ -89,7 +91,7 @@ module Bash
|
|
|
89
91
|
return unless variable_assignment?
|
|
90
92
|
|
|
91
93
|
# In bash tree-sitter, variable name is a child of type 'variable_name'
|
|
92
|
-
name_node = find_child_by_type(
|
|
94
|
+
name_node = find_child_by_type('variable_name')
|
|
93
95
|
node_text(name_node) if name_node
|
|
94
96
|
end
|
|
95
97
|
|
|
@@ -107,6 +109,18 @@ module Bash
|
|
|
107
109
|
nil
|
|
108
110
|
end
|
|
109
111
|
|
|
112
|
+
def start_byte = node.start_byte
|
|
113
|
+
def end_byte = node.end_byte
|
|
114
|
+
def source_text = @source.byteslice(start_byte...end_byte).to_s
|
|
115
|
+
|
|
116
|
+
def semantic_tree
|
|
117
|
+
semantic_node(node)
|
|
118
|
+
end
|
|
119
|
+
|
|
120
|
+
def heredoc?
|
|
121
|
+
descendant_type?(node, %w[heredoc_redirect heredoc_body])
|
|
122
|
+
end
|
|
123
|
+
|
|
110
124
|
# Find a child by field name
|
|
111
125
|
# @param field_name [String] Field name to look for
|
|
112
126
|
# @return [TreeSitter::Node, nil]
|
|
@@ -139,44 +153,49 @@ module Bash
|
|
|
139
153
|
node_type = node.type.to_s
|
|
140
154
|
|
|
141
155
|
case node_type
|
|
142
|
-
when
|
|
156
|
+
when 'program'
|
|
143
157
|
# Root node - signature based on direct children structure
|
|
144
158
|
child_types = []
|
|
145
|
-
node.each { |child| child_types << child.type.to_s unless child.type.to_s ==
|
|
159
|
+
node.each { |child| child_types << child.type.to_s unless child.type.to_s == 'comment' }
|
|
146
160
|
[:program, child_types.length]
|
|
147
|
-
when
|
|
161
|
+
when 'function_definition'
|
|
148
162
|
# Functions are identified by their name
|
|
149
163
|
name = function_name
|
|
150
164
|
[:function_definition, name]
|
|
151
|
-
when
|
|
165
|
+
when 'variable_assignment'
|
|
152
166
|
# Variable assignments are identified by variable name
|
|
153
167
|
name = variable_name
|
|
154
168
|
[:variable_assignment, name]
|
|
155
|
-
when
|
|
156
|
-
# Commands identified by their command name
|
|
169
|
+
when 'command'
|
|
170
|
+
# Commands identified by their command name and arguments.
|
|
171
|
+
# Arguments are included so that `PATH_add exe` and `PATH_add bin`
|
|
172
|
+
# get distinct signatures, while `echo "hello"` repeated twice gets
|
|
173
|
+
# the same signature — the resolver handles positional matching for
|
|
174
|
+
# nodes with identical signatures.
|
|
157
175
|
name = command_name
|
|
158
|
-
|
|
159
|
-
|
|
176
|
+
args = extract_command_arguments(node)
|
|
177
|
+
[:command, name, args, extract_command_signature_context(node)]
|
|
178
|
+
when 'if_statement'
|
|
160
179
|
# If statements identified by their condition pattern
|
|
161
180
|
condition = extract_condition_pattern(node)
|
|
162
181
|
[:if_statement, condition]
|
|
163
|
-
when
|
|
182
|
+
when 'for_statement', 'c_style_for_statement'
|
|
164
183
|
# For loops identified by their loop variable
|
|
165
184
|
var = extract_loop_variable(node)
|
|
166
185
|
[:for_statement, var]
|
|
167
|
-
when
|
|
186
|
+
when 'while_statement'
|
|
168
187
|
# While loops identified by condition
|
|
169
188
|
condition = extract_condition_pattern(node)
|
|
170
189
|
[:while_statement, condition]
|
|
171
|
-
when
|
|
190
|
+
when 'case_statement'
|
|
172
191
|
# Case statements identified by the expression being matched
|
|
173
192
|
expr = extract_case_expression(node)
|
|
174
193
|
[:case_statement, expr]
|
|
175
|
-
when
|
|
194
|
+
when 'pipeline'
|
|
176
195
|
# Pipelines identified by command names in order
|
|
177
196
|
commands = extract_pipeline_commands(node)
|
|
178
197
|
[:pipeline, commands]
|
|
179
|
-
when
|
|
198
|
+
when 'comment'
|
|
180
199
|
# Comments identified by their content
|
|
181
200
|
[:comment, node_text(node).strip]
|
|
182
201
|
else
|
|
@@ -188,38 +207,72 @@ module Bash
|
|
|
188
207
|
|
|
189
208
|
private
|
|
190
209
|
|
|
210
|
+
def semantic_node(value)
|
|
211
|
+
children = value.children
|
|
212
|
+
return [value.type.to_s, @source.byteslice(value.start_byte...value.end_byte).to_s] if children.empty?
|
|
213
|
+
|
|
214
|
+
[value.type.to_s, children.select(&:named?).map { |child| semantic_node(child) }]
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def descendant_type?(value, types)
|
|
218
|
+
return true if types.include?(value.type.to_s)
|
|
219
|
+
|
|
220
|
+
value.children.any? { |child| descendant_type?(child, types) }
|
|
221
|
+
end
|
|
222
|
+
|
|
191
223
|
def extract_command_signature_context(node)
|
|
192
224
|
# Extract additional context like redirections
|
|
193
225
|
redirections = []
|
|
194
226
|
node.each do |child|
|
|
195
|
-
if child.type.to_s.include?(
|
|
196
|
-
redirections << child.type.to_s
|
|
197
|
-
end
|
|
227
|
+
redirections << child.type.to_s if child.type.to_s.include?('redirect')
|
|
198
228
|
end
|
|
199
229
|
redirections.empty? ? nil : redirections.sort
|
|
200
230
|
end
|
|
201
231
|
|
|
232
|
+
# Extract argument words from a command node.
|
|
233
|
+
# Returns the argument text values (everything after the command name).
|
|
234
|
+
#
|
|
235
|
+
# @param node [Object] A tree-sitter command node
|
|
236
|
+
# @return [Array<String>, nil] Argument values, or nil if none
|
|
237
|
+
def extract_command_arguments(node)
|
|
238
|
+
args = []
|
|
239
|
+
found_command_name = false
|
|
240
|
+
node.each do |child|
|
|
241
|
+
type_s = child.type.to_s
|
|
242
|
+
# Skip comments and redirections
|
|
243
|
+
next if %w[comment file_redirect heredoc_redirect].include?(type_s)
|
|
244
|
+
|
|
245
|
+
if !found_command_name && %w[word command_name].include?(type_s)
|
|
246
|
+
# First word/command_name is the command itself, skip it
|
|
247
|
+
found_command_name = true
|
|
248
|
+
next
|
|
249
|
+
end
|
|
250
|
+
|
|
251
|
+
# Everything after the command name is an argument
|
|
252
|
+
args << node_text(child) if found_command_name
|
|
253
|
+
end
|
|
254
|
+
args.empty? ? nil : args
|
|
255
|
+
end
|
|
256
|
+
|
|
202
257
|
def extract_condition_pattern(node)
|
|
203
258
|
# Try to extract the test/condition from if/while statements
|
|
204
259
|
# Look for test_command, compound_statement, etc.
|
|
205
260
|
node.each do |child|
|
|
206
|
-
if %w[test_command bracket_command].include?(child.type.to_s)
|
|
207
|
-
return node_text(child).slice(0, 100).strip
|
|
208
|
-
end
|
|
261
|
+
return node_text(child).slice(0, 100).strip if %w[test_command bracket_command].include?(child.type.to_s)
|
|
209
262
|
end
|
|
210
263
|
nil
|
|
211
264
|
end
|
|
212
265
|
|
|
213
266
|
def extract_loop_variable(node)
|
|
214
267
|
# Extract the loop variable from for statements
|
|
215
|
-
var_node = node.each.find { |child| child.type.to_s ==
|
|
268
|
+
var_node = node.each.find { |child| child.type.to_s == 'variable_name' }
|
|
216
269
|
node_text(var_node) if var_node
|
|
217
270
|
end
|
|
218
271
|
|
|
219
272
|
def extract_case_expression(node)
|
|
220
273
|
# Extract the expression being matched in a case statement
|
|
221
274
|
node.each do |child|
|
|
222
|
-
return node_text(child).slice(0, 50).strip if
|
|
275
|
+
return node_text(child).slice(0, 50).strip if %w[word variable_name].include?(child.type.to_s)
|
|
223
276
|
end
|
|
224
277
|
nil
|
|
225
278
|
end
|
|
@@ -228,11 +281,11 @@ module Bash
|
|
|
228
281
|
# Extract command names from a pipeline
|
|
229
282
|
commands = []
|
|
230
283
|
node.each do |child|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
284
|
+
next unless child.type.to_s == 'command'
|
|
285
|
+
|
|
286
|
+
wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
|
|
287
|
+
cmd_name = wrapper.command_name
|
|
288
|
+
commands << cmd_name if cmd_name
|
|
236
289
|
end
|
|
237
290
|
commands
|
|
238
291
|
end
|