emerge 0.2.2 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|
|