bash-merge 2.0.5 → 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.
- checksums.yaml +4 -4
- checksums.yaml.gz.sig +0 -0
- data/LICENSE.md +13 -0
- data/README.md +105 -670
- 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 +62 -36
- data/lib/bash/merge/smart_merger.rb +641 -32
- data/lib/bash/merge/version.rb +5 -4
- data/lib/bash/merge.rb +116 -25
- data/lib/bash-merge.rb +7 -1
- data/sig/bash/merge.rbs +3 -255
- data.tar.gz.sig +0 -0
- metadata +85 -84
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -186
- 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
|
|
@@ -22,19 +22,19 @@ module Bash
|
|
|
22
22
|
# Check if this is a function definition
|
|
23
23
|
# @return [Boolean]
|
|
24
24
|
def function_definition?
|
|
25
|
-
@node.type.to_s ==
|
|
25
|
+
@node.type.to_s == 'function_definition'
|
|
26
26
|
end
|
|
27
27
|
|
|
28
28
|
# Check if this is a variable assignment
|
|
29
29
|
# @return [Boolean]
|
|
30
30
|
def variable_assignment?
|
|
31
|
-
@node.type.to_s ==
|
|
31
|
+
@node.type.to_s == 'variable_assignment'
|
|
32
32
|
end
|
|
33
33
|
|
|
34
34
|
# Check if this is an if statement
|
|
35
35
|
# @return [Boolean]
|
|
36
36
|
def if_statement?
|
|
37
|
-
@node.type.to_s ==
|
|
37
|
+
@node.type.to_s == 'if_statement'
|
|
38
38
|
end
|
|
39
39
|
|
|
40
40
|
# Check if this is a for loop
|
|
@@ -46,31 +46,31 @@ module Bash
|
|
|
46
46
|
# Check if this is a while loop
|
|
47
47
|
# @return [Boolean]
|
|
48
48
|
def while_statement?
|
|
49
|
-
@node.type.to_s ==
|
|
49
|
+
@node.type.to_s == 'while_statement'
|
|
50
50
|
end
|
|
51
51
|
|
|
52
52
|
# Check if this is a case statement
|
|
53
53
|
# @return [Boolean]
|
|
54
54
|
def case_statement?
|
|
55
|
-
@node.type.to_s ==
|
|
55
|
+
@node.type.to_s == 'case_statement'
|
|
56
56
|
end
|
|
57
57
|
|
|
58
58
|
# Check if this is a command
|
|
59
59
|
# @return [Boolean]
|
|
60
60
|
def command?
|
|
61
|
-
@node.type.to_s ==
|
|
61
|
+
@node.type.to_s == 'command'
|
|
62
62
|
end
|
|
63
63
|
|
|
64
64
|
# Check if this is a pipeline
|
|
65
65
|
# @return [Boolean]
|
|
66
66
|
def pipeline?
|
|
67
|
-
@node.type.to_s ==
|
|
67
|
+
@node.type.to_s == 'pipeline'
|
|
68
68
|
end
|
|
69
69
|
|
|
70
70
|
# Check if this is a comment
|
|
71
71
|
# @return [Boolean]
|
|
72
72
|
def comment?
|
|
73
|
-
@node.type.to_s ==
|
|
73
|
+
@node.type.to_s == 'comment'
|
|
74
74
|
end
|
|
75
75
|
|
|
76
76
|
# Get the function name if this is a function definition
|
|
@@ -79,7 +79,7 @@ module Bash
|
|
|
79
79
|
return unless function_definition?
|
|
80
80
|
|
|
81
81
|
# In bash tree-sitter, function name is in a 'name' or 'word' child
|
|
82
|
-
name_node = find_child_by_type(
|
|
82
|
+
name_node = find_child_by_type('word') || find_child_by_field('name')
|
|
83
83
|
node_text(name_node) if name_node
|
|
84
84
|
end
|
|
85
85
|
|
|
@@ -89,7 +89,7 @@ module Bash
|
|
|
89
89
|
return unless variable_assignment?
|
|
90
90
|
|
|
91
91
|
# In bash tree-sitter, variable name is a child of type 'variable_name'
|
|
92
|
-
name_node = find_child_by_type(
|
|
92
|
+
name_node = find_child_by_type('variable_name')
|
|
93
93
|
node_text(name_node) if name_node
|
|
94
94
|
end
|
|
95
95
|
|
|
@@ -139,44 +139,49 @@ module Bash
|
|
|
139
139
|
node_type = node.type.to_s
|
|
140
140
|
|
|
141
141
|
case node_type
|
|
142
|
-
when
|
|
142
|
+
when 'program'
|
|
143
143
|
# Root node - signature based on direct children structure
|
|
144
144
|
child_types = []
|
|
145
|
-
node.each { |child| child_types << child.type.to_s unless child.type.to_s ==
|
|
145
|
+
node.each { |child| child_types << child.type.to_s unless child.type.to_s == 'comment' }
|
|
146
146
|
[:program, child_types.length]
|
|
147
|
-
when
|
|
147
|
+
when 'function_definition'
|
|
148
148
|
# Functions are identified by their name
|
|
149
149
|
name = function_name
|
|
150
150
|
[:function_definition, name]
|
|
151
|
-
when
|
|
151
|
+
when 'variable_assignment'
|
|
152
152
|
# Variable assignments are identified by variable name
|
|
153
153
|
name = variable_name
|
|
154
154
|
[:variable_assignment, name]
|
|
155
|
-
when
|
|
156
|
-
# Commands identified by their command name
|
|
155
|
+
when 'command'
|
|
156
|
+
# Commands identified by their command name and arguments.
|
|
157
|
+
# Arguments are included so that `PATH_add exe` and `PATH_add bin`
|
|
158
|
+
# get distinct signatures, while `echo "hello"` repeated twice gets
|
|
159
|
+
# the same signature — the resolver handles positional matching for
|
|
160
|
+
# nodes with identical signatures.
|
|
157
161
|
name = command_name
|
|
158
|
-
|
|
159
|
-
|
|
162
|
+
args = extract_command_arguments(node)
|
|
163
|
+
[:command, name, args, extract_command_signature_context(node)]
|
|
164
|
+
when 'if_statement'
|
|
160
165
|
# If statements identified by their condition pattern
|
|
161
166
|
condition = extract_condition_pattern(node)
|
|
162
167
|
[:if_statement, condition]
|
|
163
|
-
when
|
|
168
|
+
when 'for_statement', 'c_style_for_statement'
|
|
164
169
|
# For loops identified by their loop variable
|
|
165
170
|
var = extract_loop_variable(node)
|
|
166
171
|
[:for_statement, var]
|
|
167
|
-
when
|
|
172
|
+
when 'while_statement'
|
|
168
173
|
# While loops identified by condition
|
|
169
174
|
condition = extract_condition_pattern(node)
|
|
170
175
|
[:while_statement, condition]
|
|
171
|
-
when
|
|
176
|
+
when 'case_statement'
|
|
172
177
|
# Case statements identified by the expression being matched
|
|
173
178
|
expr = extract_case_expression(node)
|
|
174
179
|
[:case_statement, expr]
|
|
175
|
-
when
|
|
180
|
+
when 'pipeline'
|
|
176
181
|
# Pipelines identified by command names in order
|
|
177
182
|
commands = extract_pipeline_commands(node)
|
|
178
183
|
[:pipeline, commands]
|
|
179
|
-
when
|
|
184
|
+
when 'comment'
|
|
180
185
|
# Comments identified by their content
|
|
181
186
|
[:comment, node_text(node).strip]
|
|
182
187
|
else
|
|
@@ -192,34 +197,55 @@ module Bash
|
|
|
192
197
|
# Extract additional context like redirections
|
|
193
198
|
redirections = []
|
|
194
199
|
node.each do |child|
|
|
195
|
-
if child.type.to_s.include?(
|
|
196
|
-
redirections << child.type.to_s
|
|
197
|
-
end
|
|
200
|
+
redirections << child.type.to_s if child.type.to_s.include?('redirect')
|
|
198
201
|
end
|
|
199
202
|
redirections.empty? ? nil : redirections.sort
|
|
200
203
|
end
|
|
201
204
|
|
|
205
|
+
# Extract argument words from a command node.
|
|
206
|
+
# Returns the argument text values (everything after the command name).
|
|
207
|
+
#
|
|
208
|
+
# @param node [Object] A tree-sitter command node
|
|
209
|
+
# @return [Array<String>, nil] Argument values, or nil if none
|
|
210
|
+
def extract_command_arguments(node)
|
|
211
|
+
args = []
|
|
212
|
+
found_command_name = false
|
|
213
|
+
node.each do |child|
|
|
214
|
+
type_s = child.type.to_s
|
|
215
|
+
# Skip comments and redirections
|
|
216
|
+
next if %w[comment file_redirect heredoc_redirect].include?(type_s)
|
|
217
|
+
|
|
218
|
+
if !found_command_name && %w[word command_name].include?(type_s)
|
|
219
|
+
# First word/command_name is the command itself, skip it
|
|
220
|
+
found_command_name = true
|
|
221
|
+
next
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
# Everything after the command name is an argument
|
|
225
|
+
args << node_text(child) if found_command_name
|
|
226
|
+
end
|
|
227
|
+
args.empty? ? nil : args
|
|
228
|
+
end
|
|
229
|
+
|
|
202
230
|
def extract_condition_pattern(node)
|
|
203
231
|
# Try to extract the test/condition from if/while statements
|
|
204
232
|
# Look for test_command, compound_statement, etc.
|
|
205
233
|
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
|
|
234
|
+
return node_text(child).slice(0, 100).strip if %w[test_command bracket_command].include?(child.type.to_s)
|
|
209
235
|
end
|
|
210
236
|
nil
|
|
211
237
|
end
|
|
212
238
|
|
|
213
239
|
def extract_loop_variable(node)
|
|
214
240
|
# Extract the loop variable from for statements
|
|
215
|
-
var_node = node.each.find { |child| child.type.to_s ==
|
|
241
|
+
var_node = node.each.find { |child| child.type.to_s == 'variable_name' }
|
|
216
242
|
node_text(var_node) if var_node
|
|
217
243
|
end
|
|
218
244
|
|
|
219
245
|
def extract_case_expression(node)
|
|
220
246
|
# Extract the expression being matched in a case statement
|
|
221
247
|
node.each do |child|
|
|
222
|
-
return node_text(child).slice(0, 50).strip if
|
|
248
|
+
return node_text(child).slice(0, 50).strip if %w[word variable_name].include?(child.type.to_s)
|
|
223
249
|
end
|
|
224
250
|
nil
|
|
225
251
|
end
|
|
@@ -228,11 +254,11 @@ module Bash
|
|
|
228
254
|
# Extract command names from a pipeline
|
|
229
255
|
commands = []
|
|
230
256
|
node.each do |child|
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
257
|
+
next unless child.type.to_s == 'command'
|
|
258
|
+
|
|
259
|
+
wrapper = NodeWrapper.new(child, lines: @lines, source: @source)
|
|
260
|
+
cmd_name = wrapper.command_name
|
|
261
|
+
commands << cmd_name if cmd_name
|
|
236
262
|
end
|
|
237
263
|
commands
|
|
238
264
|
end
|