emerge 0.2.2 → 0.4.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.
- checksums.yaml +4 -4
- data/README.md +28 -3
- data/lib/commands/global_options.rb +2 -0
- data/lib/commands/order_files/download_order_files.rb +77 -0
- data/lib/commands/order_files/validate_linkmaps.rb +55 -0
- data/lib/commands/reaper/reaper.rb +201 -0
- data/lib/commands/snapshots/validate_app.rb +64 -0
- data/lib/emerge_cli.rb +18 -2
- data/lib/reaper/ast_parser.rb +188 -3
- data/lib/reaper/code_deleter.rb +263 -0
- data/lib/utils/git.rb +13 -1
- data/lib/utils/macho_parser.rb +325 -0
- data/lib/utils/network.rb +20 -13
- data/lib/utils/version_check.rb +32 -0
- data/lib/version.rb +1 -1
- data/parsers/libtree-sitter-java-darwin-arm64.dylib +0 -0
- data/parsers/libtree-sitter-java-linux-x86_64.so +0 -0
- data/parsers/libtree-sitter-kotlin-darwin-arm64.dylib +0 -0
- data/parsers/libtree-sitter-kotlin-linux-x86_64.so +0 -0
- data/parsers/libtree-sitter-swift-darwin-arm64.dylib +0 -0
- data/parsers/libtree-sitter-swift-linux-x86_64.so +0 -0
- metadata +31 -12
data/lib/reaper/ast_parser.rb
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
require 'tree_sitter'
|
2
2
|
|
3
|
-
module
|
3
|
+
module EmergeCLI
|
4
4
|
module Reaper
|
5
5
|
# Parses the AST of a given file using Tree Sitter and allows us to find usages or delete types.
|
6
6
|
# This does have a lot of limitations since it only looks at a single file at a time,
|
@@ -8,7 +8,7 @@ module Emerge
|
|
8
8
|
class AstParser
|
9
9
|
DECLARATION_NODE_TYPES = {
|
10
10
|
'swift' => %i[class_declaration protocol_declaration],
|
11
|
-
'kotlin' => %i[class_declaration protocol_declaration interface_declaration],
|
11
|
+
'kotlin' => %i[class_declaration protocol_declaration interface_declaration object_declaration],
|
12
12
|
'java' => %i[class_declaration protocol_declaration interface_declaration]
|
13
13
|
}.freeze
|
14
14
|
|
@@ -76,6 +76,7 @@ module Emerge
|
|
76
76
|
lines_to_remove = []
|
77
77
|
|
78
78
|
while (node = nodes_to_process.shift)
|
79
|
+
Logger.debug "Processing node: #{node.type} #{node_text(node)}"
|
79
80
|
if declaration_node_types.include?(node.type)
|
80
81
|
type_identifier_node = find_type_identifier(node)
|
81
82
|
if type_identifier_node && fully_qualified_type_name(type_identifier_node) == type_name
|
@@ -95,6 +96,7 @@ module Emerge
|
|
95
96
|
|
96
97
|
lines = file_contents.split("\n")
|
97
98
|
lines_to_remove.each do |range|
|
99
|
+
Logger.debug "Removing lines #{range[:start]} to #{range[:end]}"
|
98
100
|
(range[:start]..range[:end]).each { |i| lines[i] = nil }
|
99
101
|
|
100
102
|
# Remove extra newline after class declaration, but only if it's blank
|
@@ -107,7 +109,11 @@ module Emerge
|
|
107
109
|
new_tree = @parser.parse_string(nil, modified_source)
|
108
110
|
|
109
111
|
return nil if only_comments_and_imports?(TreeSitter::TreeCursor.new(new_tree.root_node))
|
110
|
-
|
112
|
+
|
113
|
+
# Preserve original newline state
|
114
|
+
had_final_newline = file_contents.end_with?("\n")
|
115
|
+
modified_source = modified_source.rstrip
|
116
|
+
had_final_newline ? "#{modified_source}\n" : modified_source
|
111
117
|
end
|
112
118
|
|
113
119
|
# Finds all usages of a given type in a file.
|
@@ -138,9 +144,42 @@ module Emerge
|
|
138
144
|
usages
|
139
145
|
end
|
140
146
|
|
147
|
+
def delete_usage(file_contents:, type_name:)
|
148
|
+
@current_file_contents = file_contents
|
149
|
+
tree = @parser.parse_string(nil, file_contents)
|
150
|
+
cursor = TreeSitter::TreeCursor.new(tree.root_node)
|
151
|
+
nodes_to_process = [cursor.current_node]
|
152
|
+
nodes_to_remove = []
|
153
|
+
|
154
|
+
Logger.debug "Starting to scan for usages of #{type_name}"
|
155
|
+
|
156
|
+
while (node = nodes_to_process.shift)
|
157
|
+
identifier_type = identifier_node_types.include?(node.type)
|
158
|
+
if identifier_type && node_text(node) == type_name
|
159
|
+
Logger.debug "Found usage of #{type_name} in node type: #{node.type}"
|
160
|
+
removable_node = find_removable_parent(node)
|
161
|
+
if removable_node
|
162
|
+
Logger.debug "Will remove parent node of type: #{removable_node.type}"
|
163
|
+
Logger.debug "Node text to remove: #{node_text(removable_node)}"
|
164
|
+
nodes_to_remove << removable_node
|
165
|
+
else
|
166
|
+
Logger.debug 'No suitable parent node found for removal'
|
167
|
+
end
|
168
|
+
end
|
169
|
+
|
170
|
+
node.each { |child| nodes_to_process.push(child) }
|
171
|
+
end
|
172
|
+
|
173
|
+
return file_contents if nodes_to_remove.empty?
|
174
|
+
|
175
|
+
Logger.debug "Found #{nodes_to_remove.length} nodes to remove"
|
176
|
+
remove_nodes_from_content(file_contents, nodes_to_remove)
|
177
|
+
end
|
178
|
+
|
141
179
|
private
|
142
180
|
|
143
181
|
def remove_node(node, lines_to_remove)
|
182
|
+
Logger.debug "Removing node: #{node.type}"
|
144
183
|
start_position = node.start_point.row
|
145
184
|
end_position = node.end_point.row
|
146
185
|
lines_to_remove << { start: start_position, end: end_position }
|
@@ -188,6 +227,7 @@ module Emerge
|
|
188
227
|
parent = find_parent_type_declaration(parent)
|
189
228
|
end
|
190
229
|
|
230
|
+
Logger.debug "Fully qualified type name: #{class_name}"
|
191
231
|
class_name
|
192
232
|
end
|
193
233
|
|
@@ -229,6 +269,151 @@ module Emerge
|
|
229
269
|
end_byte = node.end_byte
|
230
270
|
@current_file_contents[start_byte...end_byte]
|
231
271
|
end
|
272
|
+
|
273
|
+
def find_removable_parent(node)
|
274
|
+
current = node
|
275
|
+
Logger.debug "Finding removable parent for node type: #{node.type}"
|
276
|
+
|
277
|
+
while current && !current.null?
|
278
|
+
Logger.debug "Checking parent node type: #{current.type}"
|
279
|
+
case current.type
|
280
|
+
when :variable_declaration, # var foo: DeletedType
|
281
|
+
:parameter, # func example(param: DeletedType)
|
282
|
+
:type_annotation, # : DeletedType
|
283
|
+
:argument, # functionCall(param: DeletedType)
|
284
|
+
:import_declaration # import DeletedType
|
285
|
+
Logger.debug "Found removable parent node of type: #{current.type}"
|
286
|
+
return current
|
287
|
+
when :navigation_expression # NetworkDebugger.printStats
|
288
|
+
result = handle_navigation_expression(current)
|
289
|
+
return result if result
|
290
|
+
when :class_declaration, :function_declaration, :method_declaration
|
291
|
+
Logger.debug "Reached structural element, stopping at: #{current.type}"
|
292
|
+
break
|
293
|
+
end
|
294
|
+
current = current.parent
|
295
|
+
end
|
296
|
+
|
297
|
+
Logger.debug 'No better parent found, returning original node'
|
298
|
+
node
|
299
|
+
end
|
300
|
+
|
301
|
+
def handle_navigation_expression(navigation_node)
|
302
|
+
# If this navigation expression is part of a call, remove the entire call
|
303
|
+
parent_call = navigation_node.parent
|
304
|
+
return nil unless parent_call && parent_call.type == :call_expression
|
305
|
+
|
306
|
+
Logger.debug 'Found call expression containing navigation expression'
|
307
|
+
# Check if this call is the only statement in an if condition
|
308
|
+
if_statement = find_parent_if_statement(parent_call)
|
309
|
+
if if_statement && contains_single_statement?(if_statement)
|
310
|
+
Logger.debug 'Found if statement with single call, removing entire if block'
|
311
|
+
return if_statement
|
312
|
+
end
|
313
|
+
parent_call
|
314
|
+
end
|
315
|
+
|
316
|
+
def find_parent_if_statement(node)
|
317
|
+
current = node
|
318
|
+
Logger.debug "Looking for parent if statement starting from node type: #{node.type}"
|
319
|
+
while current && !current.null?
|
320
|
+
Logger.debug " Checking node type: #{current.type}"
|
321
|
+
if current.type == :if_statement
|
322
|
+
Logger.debug ' Found parent if statement'
|
323
|
+
return current
|
324
|
+
end
|
325
|
+
current = current.parent
|
326
|
+
end
|
327
|
+
Logger.debug ' No parent if statement found'
|
328
|
+
nil
|
329
|
+
end
|
330
|
+
|
331
|
+
def contains_single_statement?(if_statement)
|
332
|
+
Logger.debug 'Checking if statement for single statement'
|
333
|
+
# Find the block/body of the if statement - try different field names based on language
|
334
|
+
block = if_statement.child_by_field_name('consequence') ||
|
335
|
+
if_statement.child_by_field_name('body') ||
|
336
|
+
if_statement.find { |child| child.type == :statements }
|
337
|
+
|
338
|
+
unless block
|
339
|
+
Logger.debug ' No block found in if statement. Node structure:'
|
340
|
+
Logger.debug " If statement type: #{if_statement.type}"
|
341
|
+
Logger.debug ' Children types:'
|
342
|
+
if_statement.each do |child|
|
343
|
+
Logger.debug " - #{child.type} (text: #{node_text(child)[0..50]}...)"
|
344
|
+
end
|
345
|
+
return false
|
346
|
+
end
|
347
|
+
|
348
|
+
Logger.debug " Found block of type: #{block.type}"
|
349
|
+
|
350
|
+
relevant_children = block.reject do |child|
|
351
|
+
%i[comment line_break whitespace].include?(child.type)
|
352
|
+
end
|
353
|
+
|
354
|
+
Logger.debug " Found #{relevant_children.length} significant children in if block"
|
355
|
+
relevant_children.each do |child|
|
356
|
+
Logger.debug " Child type: #{child.type}, text: #{node_text(child)[0..50]}..."
|
357
|
+
end
|
358
|
+
|
359
|
+
relevant_children.length == 1
|
360
|
+
end
|
361
|
+
|
362
|
+
def remove_nodes_from_content(content, nodes)
|
363
|
+
# Sort nodes by their position in reverse order to avoid offset issues
|
364
|
+
nodes.sort_by! { |n| -n.start_byte }
|
365
|
+
|
366
|
+
# Check if original file had final newline
|
367
|
+
had_final_newline = content.end_with?("\n")
|
368
|
+
|
369
|
+
# Remove each node and clean up surrounding blank lines
|
370
|
+
modified_contents = content.dup
|
371
|
+
nodes.each do |node|
|
372
|
+
modified_contents = remove_single_node(modified_contents, node)
|
373
|
+
end
|
374
|
+
|
375
|
+
# Restore the original newline state at the end of the file
|
376
|
+
modified_contents.chomp!
|
377
|
+
had_final_newline ? "#{modified_contents}\n" : modified_contents
|
378
|
+
end
|
379
|
+
|
380
|
+
def remove_single_node(content, node)
|
381
|
+
had_final_newline = content.end_with?("\n")
|
382
|
+
|
383
|
+
# Remove the node's content
|
384
|
+
start_byte = node.start_byte
|
385
|
+
end_byte = node.end_byte
|
386
|
+
Logger.debug "Removing text: #{content[start_byte...end_byte]}"
|
387
|
+
content[start_byte...end_byte] = ''
|
388
|
+
|
389
|
+
# Clean up any blank lines created by the removal
|
390
|
+
content = cleanup_blank_lines(content, node.start_point.row, node.end_point.row)
|
391
|
+
|
392
|
+
had_final_newline ? "#{content}\n" : content
|
393
|
+
end
|
394
|
+
|
395
|
+
def cleanup_blank_lines(content, start_line, end_line)
|
396
|
+
lines = content.split("\n")
|
397
|
+
|
398
|
+
# Check for consecutive blank lines around the removed content
|
399
|
+
lines[start_line - 1] = nil if consecutive_blank_lines?(lines, start_line, end_line)
|
400
|
+
|
401
|
+
# Remove any blank lines left in the removed node's place
|
402
|
+
(start_line..end_line).each do |i|
|
403
|
+
lines[i] = nil if lines[i]&.match?(/^\s*$/)
|
404
|
+
end
|
405
|
+
|
406
|
+
lines.compact.join("\n")
|
407
|
+
end
|
408
|
+
|
409
|
+
def consecutive_blank_lines?(lines, start_line, end_line)
|
410
|
+
return false unless start_line > 0 && end_line + 1 < lines.length
|
411
|
+
|
412
|
+
prev_line = lines[start_line - 1]
|
413
|
+
next_line = lines[end_line + 1]
|
414
|
+
|
415
|
+
prev_line&.match?(/^\s*$/) && next_line&.match?(/^\s*$/)
|
416
|
+
end
|
232
417
|
end
|
233
418
|
end
|
234
419
|
end
|
@@ -0,0 +1,263 @@
|
|
1
|
+
require 'xcodeproj'
|
2
|
+
|
3
|
+
module EmergeCLI
|
4
|
+
module Reaper
|
5
|
+
class CodeDeleter
|
6
|
+
def initialize(project_root:, platform:, profiler:, skip_delete_usages: false)
|
7
|
+
@project_root = File.expand_path(project_root)
|
8
|
+
@platform = platform
|
9
|
+
@profiler = profiler
|
10
|
+
@skip_delete_usages = skip_delete_usages
|
11
|
+
Logger.debug "Initialized CodeDeleter with project root: #{@project_root}, platform: #{@platform}"
|
12
|
+
end
|
13
|
+
|
14
|
+
def delete_types(types)
|
15
|
+
Logger.debug "Project root: #{@project_root}"
|
16
|
+
|
17
|
+
types.each do |class_info|
|
18
|
+
Logger.info "Deleting #{class_info['class_name']}"
|
19
|
+
|
20
|
+
type_name = parse_type_name(class_info['class_name'])
|
21
|
+
Logger.debug "Parsed type name: #{type_name}"
|
22
|
+
|
23
|
+
# Remove line number from path if present
|
24
|
+
paths = class_info['paths']&.map { |path| path.sub(/:\d+$/, '') }
|
25
|
+
found_usages = @profiler.measure('find_type_in_project') do
|
26
|
+
find_type_in_project(type_name)
|
27
|
+
end
|
28
|
+
|
29
|
+
if paths.nil? || paths.empty?
|
30
|
+
Logger.info "No paths provided for #{type_name}, using found usages instead..."
|
31
|
+
paths = found_usages
|
32
|
+
.select { |usage| usage[:usages].any? { |u| u[:usage_type] == 'declaration' } }
|
33
|
+
.map { |usage| usage[:path] }
|
34
|
+
if paths.empty?
|
35
|
+
Logger.warn "Could not find any files containing #{type_name}"
|
36
|
+
next
|
37
|
+
end
|
38
|
+
Logger.info "Found #{type_name} in: #{paths.join(', ')}"
|
39
|
+
end
|
40
|
+
|
41
|
+
# First pass: Delete declarations
|
42
|
+
paths.each do |path|
|
43
|
+
Logger.debug "Processing path: #{path}"
|
44
|
+
@profiler.measure('delete_type_from_file') do
|
45
|
+
delete_type_from_file(path, type_name)
|
46
|
+
end
|
47
|
+
end
|
48
|
+
|
49
|
+
# Second pass: Delete remaining usages (unless skipped)
|
50
|
+
if @skip_delete_usages
|
51
|
+
Logger.info 'Skipping delete usages'
|
52
|
+
else
|
53
|
+
identifier_usages = found_usages.select do |usage|
|
54
|
+
usage[:usages].any? { |u| u[:usage_type] == 'identifier' }
|
55
|
+
end
|
56
|
+
identifier_usage_paths = identifier_usages.map { |usage| usage[:path] }.uniq
|
57
|
+
if identifier_usage_paths.empty?
|
58
|
+
Logger.info 'No identifier usages found, skipping delete usages'
|
59
|
+
else
|
60
|
+
identifier_usage_paths.each do |path|
|
61
|
+
Logger.debug "Processing usages in path: #{path}"
|
62
|
+
@profiler.measure('delete_usages_from_file') do
|
63
|
+
delete_usages_from_file(path, type_name)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
private
|
72
|
+
|
73
|
+
def parse_type_name(type_name)
|
74
|
+
# Remove first module prefix for Swift types if present
|
75
|
+
if @platform == 'ios' && type_name.include?('.')
|
76
|
+
type_name.split('.')[1..].join('.')
|
77
|
+
# For Android, strip package name and just use the class name
|
78
|
+
elsif @platform == 'android' && type_name.include?('.')
|
79
|
+
# rubocop:disable Layout/LineLength
|
80
|
+
# Handle cases like "com.emergetools.hackernews.data.remote.ItemResponse $NullResponse (HackerNewsBaseClient.kt)"
|
81
|
+
# rubocop:enable Layout/LineLength
|
82
|
+
has_nested_class = type_name.include?('$')
|
83
|
+
parts = type_name.split
|
84
|
+
if parts.length == 0
|
85
|
+
type_name
|
86
|
+
elsif has_nested_class && parts.length > 1
|
87
|
+
base_name = parts[0].split('.').last
|
88
|
+
nested_class = parts[1].match(/\$(.+)/).captures.first
|
89
|
+
"#{base_name}.#{nested_class}"
|
90
|
+
else
|
91
|
+
parts[0].split('.').last
|
92
|
+
end
|
93
|
+
else
|
94
|
+
type_name
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def delete_type_from_file(path, type_name)
|
99
|
+
full_path = resolve_file_path(path)
|
100
|
+
return unless full_path
|
101
|
+
|
102
|
+
Logger.debug "Processing file: #{full_path}"
|
103
|
+
begin
|
104
|
+
original_contents = @profiler.measure('read_file') { File.read(full_path) }
|
105
|
+
parser = make_parser_for_file(full_path)
|
106
|
+
modified_contents = @profiler.measure('parse_and_delete_type') do
|
107
|
+
parser.delete_type(
|
108
|
+
file_contents: original_contents,
|
109
|
+
type_name: type_name
|
110
|
+
)
|
111
|
+
end
|
112
|
+
|
113
|
+
if modified_contents.nil?
|
114
|
+
@profiler.measure('delete_file') do
|
115
|
+
File.delete(full_path)
|
116
|
+
end
|
117
|
+
if parser.language == 'swift'
|
118
|
+
@profiler.measure('delete_type_from_xcode_project') do
|
119
|
+
delete_type_from_xcode_project(full_path)
|
120
|
+
end
|
121
|
+
end
|
122
|
+
Logger.info "Deleted file #{full_path} as it only contained #{type_name}"
|
123
|
+
elsif modified_contents != original_contents
|
124
|
+
@profiler.measure('write_file') do
|
125
|
+
File.write(full_path, modified_contents)
|
126
|
+
end
|
127
|
+
Logger.info "Successfully deleted #{type_name} from #{full_path}"
|
128
|
+
else
|
129
|
+
Logger.warn "No changes made to #{full_path} for #{type_name}"
|
130
|
+
end
|
131
|
+
rescue StandardError => e
|
132
|
+
Logger.error "Failed to delete #{type_name} from #{full_path}: #{e.message}"
|
133
|
+
Logger.error e.backtrace.join("\n")
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
def delete_type_from_xcode_project(file_path)
|
138
|
+
xcodeproj_path = Dir.glob(File.join(@project_root, '**/*.xcodeproj')).first
|
139
|
+
if xcodeproj_path.nil?
|
140
|
+
Logger.warn "No Xcode project found in #{@project_root}"
|
141
|
+
return
|
142
|
+
end
|
143
|
+
|
144
|
+
begin
|
145
|
+
project = Xcodeproj::Project.open(xcodeproj_path)
|
146
|
+
relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(@project_root)).to_s
|
147
|
+
|
148
|
+
file_ref = project.files.find { |f| f.real_path.to_s.end_with?(relative_path) }
|
149
|
+
if file_ref
|
150
|
+
file_ref.remove_from_project
|
151
|
+
project.save
|
152
|
+
Logger.info "Removed #{relative_path} from Xcode project"
|
153
|
+
else
|
154
|
+
Logger.warn "Could not find #{relative_path} in Xcode project"
|
155
|
+
end
|
156
|
+
rescue StandardError => e
|
157
|
+
Logger.error "Failed to update Xcode project: #{e.message}"
|
158
|
+
Logger.error e.backtrace.join("\n")
|
159
|
+
end
|
160
|
+
end
|
161
|
+
|
162
|
+
def find_type_in_project(type_name)
|
163
|
+
found_usages = []
|
164
|
+
source_patterns = case @platform&.downcase
|
165
|
+
when 'ios'
|
166
|
+
{ 'swift' => '**/*.swift' }
|
167
|
+
when 'android'
|
168
|
+
{
|
169
|
+
'kotlin' => '**/*.kt',
|
170
|
+
'java' => '**/*.java'
|
171
|
+
}
|
172
|
+
else
|
173
|
+
raise "Unsupported platform: #{@platform}"
|
174
|
+
end
|
175
|
+
|
176
|
+
source_patterns.each do |language, pattern|
|
177
|
+
Dir.glob(File.join(@project_root, pattern)).reject { |path| path.include?('/build/') }.each do |file_path|
|
178
|
+
Logger.debug "Scanning #{file_path} for #{type_name}"
|
179
|
+
contents = File.read(file_path)
|
180
|
+
parser = make_parser_for_file(file_path)
|
181
|
+
usages = parser.find_usages(file_contents: contents, type_name: type_name)
|
182
|
+
|
183
|
+
if usages.any?
|
184
|
+
Logger.debug "✅ Found #{type_name} in #{file_path}"
|
185
|
+
relative_path = Pathname.new(file_path).relative_path_from(Pathname.new(@project_root)).to_s
|
186
|
+
found_usages << {
|
187
|
+
path: relative_path,
|
188
|
+
usages: usages,
|
189
|
+
language: language
|
190
|
+
}
|
191
|
+
end
|
192
|
+
rescue StandardError => e
|
193
|
+
Logger.warn "Error scanning #{file_path}: #{e.message}"
|
194
|
+
end
|
195
|
+
end
|
196
|
+
|
197
|
+
found_usages
|
198
|
+
end
|
199
|
+
|
200
|
+
def delete_usages_from_file(path, type_name)
|
201
|
+
full_path = resolve_file_path(path)
|
202
|
+
return unless full_path
|
203
|
+
|
204
|
+
begin
|
205
|
+
original_contents = File.read(full_path)
|
206
|
+
parser = make_parser_for_file(full_path)
|
207
|
+
Logger.debug "Deleting usages of #{type_name} from #{full_path}"
|
208
|
+
modified_contents = parser.delete_usage(
|
209
|
+
file_contents: original_contents,
|
210
|
+
type_name: type_name
|
211
|
+
)
|
212
|
+
|
213
|
+
if modified_contents != original_contents
|
214
|
+
File.write(full_path, modified_contents)
|
215
|
+
Logger.info "Successfully removed usages of #{type_name} from #{full_path}"
|
216
|
+
end
|
217
|
+
rescue StandardError => e
|
218
|
+
Logger.error "Failed to delete usages of #{type_name} from #{full_path}: #{e.message}"
|
219
|
+
Logger.error e.backtrace.join("\n")
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def resolve_file_path(path)
|
224
|
+
# If path starts with /, treat it as relative to project root
|
225
|
+
if path.start_with?('/')
|
226
|
+
path = path[1..] # Remove leading slash
|
227
|
+
full_path = File.join(@project_root, path)
|
228
|
+
return full_path if File.exist?(full_path)
|
229
|
+
end
|
230
|
+
|
231
|
+
# Try direct path first
|
232
|
+
full_path = File.join(@project_root, path)
|
233
|
+
return full_path if File.exist?(full_path)
|
234
|
+
|
235
|
+
# If not found, search recursively
|
236
|
+
Logger.debug "File not found at #{full_path}, searching in project..."
|
237
|
+
matching_files = Dir.glob(File.join(@project_root, '**', path))
|
238
|
+
.reject { |p| p.include?('/build/') }
|
239
|
+
|
240
|
+
if matching_files.empty?
|
241
|
+
Logger.warn "Could not find #{path} in project"
|
242
|
+
return nil
|
243
|
+
elsif matching_files.length > 1
|
244
|
+
Logger.warn "Found multiple matches for #{path}: #{matching_files.join(', ')}"
|
245
|
+
Logger.warn "Using first match: #{matching_files.first}"
|
246
|
+
end
|
247
|
+
|
248
|
+
matching_files.first
|
249
|
+
end
|
250
|
+
|
251
|
+
def make_parser_for_file(file_path)
|
252
|
+
language = case File.extname(file_path)
|
253
|
+
when '.swift' then 'swift'
|
254
|
+
when '.kt' then 'kotlin'
|
255
|
+
when '.java' then 'java'
|
256
|
+
else
|
257
|
+
raise "Unsupported file type for #{file_path}"
|
258
|
+
end
|
259
|
+
AstParser.new(language)
|
260
|
+
end
|
261
|
+
end
|
262
|
+
end
|
263
|
+
end
|
data/lib/utils/git.rb
CHANGED
@@ -59,9 +59,21 @@ module EmergeCLI
|
|
59
59
|
end
|
60
60
|
|
61
61
|
def self.previous_sha
|
62
|
+
command = 'git rev-list --count HEAD'
|
63
|
+
Logger.debug command
|
64
|
+
count_stdout, _, count_status = Open3.capture3(command)
|
65
|
+
|
66
|
+
if !count_status.success? || count_stdout.strip.to_i <= 1
|
67
|
+
Logger.error 'Detected shallow clone while trying to get the previous commit. ' \
|
68
|
+
'Please clone with full history using: git clone --no-single-branch ' \
|
69
|
+
'or configure CI with fetch-depth: 0'
|
70
|
+
return nil
|
71
|
+
end
|
72
|
+
|
62
73
|
command = 'git rev-parse HEAD^'
|
63
74
|
Logger.debug command
|
64
|
-
stdout,
|
75
|
+
stdout, stderr, status = Open3.capture3(command)
|
76
|
+
Logger.error "Failed to get previous SHA: #{stdout}, #{stderr}" if !status.success?
|
65
77
|
stdout.strip if status.success?
|
66
78
|
end
|
67
79
|
|