emerge 0.2.1 → 0.3.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 +47 -1
- data/lib/commands/config/orderfiles/orderfiles_ios.rb +0 -2
- data/lib/commands/config/snapshots/snapshots_ios.rb +5 -8
- data/lib/commands/global_options.rb +2 -0
- data/lib/commands/integrate/fastlane.rb +2 -2
- data/lib/commands/reaper/reaper.rb +201 -0
- data/lib/commands/upload/snapshots/snapshots.rb +4 -4
- data/lib/emerge_cli.rb +28 -19
- data/lib/reaper/ast_parser.rb +419 -0
- data/lib/reaper/code_deleter.rb +263 -0
- data/lib/utils/git.rb +1 -1
- data/lib/utils/github.rb +3 -3
- data/lib/utils/logger.rb +2 -2
- data/lib/utils/network.rb +33 -13
- data/lib/utils/version_check.rb +32 -0
- data/lib/version.rb +2 -2
- 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 +25 -126
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: f78e2be6724a6620a5135d20910b99fe76c8a959b00f6703bdad0c0d2c043742
|
4
|
+
data.tar.gz: 8366d036be25ec0b3da721346b1c71dcfcdccd2bf7b7f2bea9f3e6838cf62a8b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 4359ea66070503dc5dc98d1040a39598df3d760bad78393724baa0f689caf18c5e26d2875375ba3751f2fb95d4641e078ff4111084c7acf51b63118955e6a3cc
|
7
|
+
data.tar.gz: 13d4cc4f14a5f6e794592ce12eb5f6acd785fa5d51fbb7d9241e69c78ab7317a5f7b374dbcf06b2bf641cff890b1a7baa941ad3c76f306e207bbc069492875f0
|
data/README.md
CHANGED
@@ -14,7 +14,7 @@ gem install emerge
|
|
14
14
|
|
15
15
|
## API Key
|
16
16
|
|
17
|
-
Follow our guide to obtain an [API key](https://docs.emergetools.com/docs/uploading-basics#obtain-an-api-key) for your organization. The API Token is used by the CLI to authenticate with the Emerge API. The CLI will automatically pick up the API key if configured as an `EMERGE_API_TOKEN` environment variable, or you can manually pass it into individual commands.
|
17
|
+
Follow our guide to obtain an [API key](https://docs.emergetools.com/docs/uploading-basics#obtain-an-api-key) for your organization. The API Token is used by the CLI to authenticate with the Emerge API. The CLI will automatically pick up the API key if configured as an `EMERGE_API_TOKEN` environment variable, or you can manually pass it into individual commands with the `--api-token` option.
|
18
18
|
|
19
19
|
## Snapshots
|
20
20
|
|
@@ -92,3 +92,49 @@ emerge upload snapshots \
|
|
92
92
|
--client-library paparazzi \
|
93
93
|
--project-root /my/awesomeapp/android/repo
|
94
94
|
```
|
95
|
+
|
96
|
+
### Using with Roborazzi
|
97
|
+
|
98
|
+
Snapshots generated via [Roborazzi](https://github.com/takahirom/roborazzi) are natively supported by the CLI by setting `--client-library roborazzi` and a `--project-root` directory. This will scan your project for all images found in `**/build/outputs/roborazzi` directories.
|
99
|
+
|
100
|
+
Example:
|
101
|
+
|
102
|
+
```shell
|
103
|
+
emerge upload snapshots \
|
104
|
+
--name "AwesomeApp Roborazzi" \
|
105
|
+
--id "com.emerge.awesomeapp.roborazzi" \
|
106
|
+
--repo-name "EmergeTools/AwesomeApp" \
|
107
|
+
--client-library roborazzi \
|
108
|
+
--project-root /my/awesomeapp/android/repo
|
109
|
+
```
|
110
|
+
|
111
|
+
## Reaper
|
112
|
+
|
113
|
+
Experimental support has been added to interactively examine [Reaper](https://docs.emergetools.com/docs/reaper) results and also **delete them from your codebase**.
|
114
|
+
|
115
|
+
Use the `reaper` subcommand to get started, e.g.:
|
116
|
+
|
117
|
+
```shell
|
118
|
+
emerge reaper --upload-id 40f1dfe7-6c57-47c3-bc52-b621aec0ba8d \
|
119
|
+
--project-root /path/to/your/repo
|
120
|
+
```
|
121
|
+
|
122
|
+
After which it will prompt you to select classes to delete.
|
123
|
+
|
124
|
+
### How it works
|
125
|
+
|
126
|
+
Under the hood we are using [Tree Sitter](https://tree-sitter.github.io/tree-sitter/) to parse your source files into an AST which is then used for deletions. There are some obvious limitations to this approach, namely that Tree Sitter is designed for source code editors and only looks at a single file at a time. We are exploring some better long-term approaches but this works well enough for now!
|
127
|
+
|
128
|
+
### Supported languages
|
129
|
+
|
130
|
+
We currently support the following languages:
|
131
|
+
|
132
|
+
- Swift
|
133
|
+
- Kotlin
|
134
|
+
- Java
|
135
|
+
|
136
|
+
Please open an issue if you need an additional language grammar.
|
137
|
+
|
138
|
+
### Building
|
139
|
+
|
140
|
+
Because many of the language grammars we use are third-party, we have to package them with our CLI tool as shared libraries for distribution. We depend on [tsdl](https://github.com/stackmystack/tsdl) to build the grammars from our `parsers.toml` file.
|
@@ -39,8 +39,6 @@ module EmergeCLI
|
|
39
39
|
format KEY=VALUE".freeze
|
40
40
|
AVAILABLE_OS_VERSIONS = ['17.2', '17.5', '18.0'].freeze
|
41
41
|
|
42
|
-
def initialize; end
|
43
|
-
|
44
42
|
def call(**options)
|
45
43
|
@options = options
|
46
44
|
before(options)
|
@@ -204,12 +202,11 @@ format KEY=VALUE".freeze
|
|
204
202
|
end
|
205
203
|
|
206
204
|
def get_parsed_previews(previews_exact, previews_regex)
|
207
|
-
excluded =
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
})
|
205
|
+
excluded = previews_exact.map do |preview|
|
206
|
+
{
|
207
|
+
'type' => 'exact',
|
208
|
+
'value' => preview
|
209
|
+
}
|
213
210
|
end
|
214
211
|
previews_regex.each do |preview|
|
215
212
|
excluded.push({
|
@@ -80,7 +80,7 @@ module EmergeCLI
|
|
80
80
|
|
81
81
|
# Add app_size lane if not present
|
82
82
|
unless current_content.match?(/^\s*lane\s*:app_size\s*do/)
|
83
|
-
app_size_lane = <<~
|
83
|
+
app_size_lane = <<~RUBY.gsub(/^/, ' ')
|
84
84
|
lane :app_size do
|
85
85
|
# NOTE: If you already have a lane setup to build your app, then you can that instead of this and call emerge() after it.
|
86
86
|
build_app(scheme: ENV["SCHEME_NAME"], export_method: "development")
|
@@ -92,7 +92,7 @@ module EmergeCLI
|
|
92
92
|
|
93
93
|
# Add snapshots lane if not present
|
94
94
|
unless current_content.match?(/^\s*lane\s*:build_upload_emerge_snapshot\s*do/)
|
95
|
-
snapshot_lane = <<~
|
95
|
+
snapshot_lane = <<~RUBY.gsub(/^/, ' ')
|
96
96
|
desc 'Build and upload snapshot build to Emerge Tools'
|
97
97
|
lane :build_upload_emerge_snapshot do
|
98
98
|
emerge_snapshot(scheme: ENV["SCHEME_NAME"])
|
@@ -0,0 +1,201 @@
|
|
1
|
+
require 'dry/cli'
|
2
|
+
require 'json'
|
3
|
+
require 'tty-prompt'
|
4
|
+
|
5
|
+
module EmergeCLI
|
6
|
+
module Commands
|
7
|
+
class Reaper < EmergeCLI::Commands::GlobalOptions
|
8
|
+
desc 'Analyze dead code from an Emerge upload'
|
9
|
+
|
10
|
+
option :upload_id, type: :string, required: true, desc: 'Upload ID to analyze'
|
11
|
+
option :project_root, type: :string, required: true,
|
12
|
+
desc: 'Root directory of the project, defaults to current directory'
|
13
|
+
|
14
|
+
option :api_token, type: :string, required: false,
|
15
|
+
desc: 'API token for authentication, defaults to ENV[EMERGE_API_TOKEN]'
|
16
|
+
|
17
|
+
option :profile, type: :boolean, default: false, desc: 'Enable performance profiling metrics'
|
18
|
+
|
19
|
+
option :skip_delete_usages, type: :boolean, default: false,
|
20
|
+
desc: 'Skip deleting usages of the type (experimental feature)'
|
21
|
+
|
22
|
+
def initialize(network: nil)
|
23
|
+
@network = network
|
24
|
+
end
|
25
|
+
|
26
|
+
def call(**options)
|
27
|
+
@options = options
|
28
|
+
@profiler = EmergeCLI::Profiler.new(enabled: options[:profile])
|
29
|
+
@prompt = TTY::Prompt.new
|
30
|
+
before(options)
|
31
|
+
success = false
|
32
|
+
|
33
|
+
begin
|
34
|
+
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
|
35
|
+
raise 'API token is required' unless api_token
|
36
|
+
|
37
|
+
@network ||= EmergeCLI::Network.new(api_token:)
|
38
|
+
project_root = @options[:project_root] || Dir.pwd
|
39
|
+
|
40
|
+
Sync do
|
41
|
+
all_data = @profiler.measure('fetch_dead_code') { fetch_all_dead_code(@options[:upload_id]) }
|
42
|
+
result = @profiler.measure('parse_dead_code') { DeadCodeResult.new(all_data) }
|
43
|
+
|
44
|
+
Logger.info result.to_s
|
45
|
+
|
46
|
+
selected_types = prompt_class_selection(result.filtered_unseen_classes, result.metadata['platform'])
|
47
|
+
Logger.info 'Selected classes:'
|
48
|
+
selected_types.each do |selected_class|
|
49
|
+
Logger.info " - #{selected_class['class_name']}"
|
50
|
+
end
|
51
|
+
|
52
|
+
confirmed = confirm_deletion(selected_types.length)
|
53
|
+
if !confirmed
|
54
|
+
Logger.info 'Operation cancelled'
|
55
|
+
return false
|
56
|
+
end
|
57
|
+
|
58
|
+
Logger.info 'Proceeding with deletion...'
|
59
|
+
platform = result.metadata['platform']
|
60
|
+
deleter = EmergeCLI::Reaper::CodeDeleter.new(
|
61
|
+
project_root: project_root,
|
62
|
+
platform: platform,
|
63
|
+
profiler: @profiler,
|
64
|
+
skip_delete_usages: options[:skip_delete_usages]
|
65
|
+
)
|
66
|
+
@profiler.measure('delete_types') { deleter.delete_types(selected_types) }
|
67
|
+
end
|
68
|
+
|
69
|
+
@profiler.report if @options[:profile]
|
70
|
+
success = true
|
71
|
+
rescue StandardError => e
|
72
|
+
Logger.error "Failed to analyze dead code: #{e.message}"
|
73
|
+
raise e
|
74
|
+
ensure
|
75
|
+
@network&.close
|
76
|
+
end
|
77
|
+
|
78
|
+
success
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
def fetch_all_dead_code(upload_id)
|
84
|
+
Logger.info 'Fetching dead code analysis (this may take a while for large codebases)...'
|
85
|
+
|
86
|
+
page = 1
|
87
|
+
combined_data = nil
|
88
|
+
|
89
|
+
loop do
|
90
|
+
response = fetch_dead_code_page(upload_id, page)
|
91
|
+
data = JSON.parse(response.read)
|
92
|
+
|
93
|
+
if combined_data.nil?
|
94
|
+
combined_data = data
|
95
|
+
else
|
96
|
+
combined_data['dead_code'].concat(data.fetch('dead_code', []))
|
97
|
+
end
|
98
|
+
|
99
|
+
current_page = data.dig('pagination', 'current_page')
|
100
|
+
total_pages = data.dig('pagination', 'total_pages')
|
101
|
+
|
102
|
+
break unless current_page && total_pages && current_page < total_pages
|
103
|
+
|
104
|
+
page += 1
|
105
|
+
Logger.info "Fetching page #{page} of #{total_pages}..."
|
106
|
+
end
|
107
|
+
|
108
|
+
combined_data
|
109
|
+
end
|
110
|
+
|
111
|
+
def fetch_dead_code_page(upload_id, page)
|
112
|
+
@network.post(
|
113
|
+
path: '/deadCode/export',
|
114
|
+
query: {
|
115
|
+
uploadId: upload_id,
|
116
|
+
page: page
|
117
|
+
},
|
118
|
+
headers: { 'Accept' => 'application/json' },
|
119
|
+
body: nil
|
120
|
+
)
|
121
|
+
end
|
122
|
+
|
123
|
+
def prompt_class_selection(unseen_classes, platform)
|
124
|
+
return nil if unseen_classes.empty?
|
125
|
+
|
126
|
+
choices = unseen_classes.map do |item|
|
127
|
+
display_name = if item['paths']&.first && platform == 'ios'
|
128
|
+
"#{item['class_name']} (#{item['paths'].first})"
|
129
|
+
else
|
130
|
+
item['class_name']
|
131
|
+
end
|
132
|
+
{
|
133
|
+
name: display_name,
|
134
|
+
value: item
|
135
|
+
}
|
136
|
+
end
|
137
|
+
|
138
|
+
@prompt.multi_select(
|
139
|
+
'Select classes to delete:'.blue,
|
140
|
+
choices,
|
141
|
+
per_page: 15,
|
142
|
+
echo: false,
|
143
|
+
filter: true,
|
144
|
+
min: 1
|
145
|
+
)
|
146
|
+
end
|
147
|
+
|
148
|
+
def confirm_deletion(count)
|
149
|
+
@prompt.yes?("Are you sure you want to delete #{count} type#{count > 1 ? 's' : ''}?")
|
150
|
+
end
|
151
|
+
|
152
|
+
class DeadCodeResult
|
153
|
+
attr_reader :metadata, :dead_code, :counts, :pagination
|
154
|
+
|
155
|
+
def initialize(data)
|
156
|
+
@metadata = data['metadata']
|
157
|
+
@dead_code = data['dead_code']
|
158
|
+
@counts = data['counts']
|
159
|
+
@pagination = data['pagination']
|
160
|
+
end
|
161
|
+
|
162
|
+
def filtered_unseen_classes
|
163
|
+
@filtered_unseen_classes ||= dead_code
|
164
|
+
.reject { |item| item['seen'] }
|
165
|
+
.reject do |item|
|
166
|
+
paths = item['paths']
|
167
|
+
next false if paths.nil? || paths.empty?
|
168
|
+
|
169
|
+
next true if paths.any? do |path|
|
170
|
+
path.include?('/SourcePackages/checkouts/') ||
|
171
|
+
path.include?('/Pods/') ||
|
172
|
+
path.include?('/Carthage/') ||
|
173
|
+
path.include?('/Vendor/') ||
|
174
|
+
path.include?('/Sources/') ||
|
175
|
+
path.include?('/DerivedSources/')
|
176
|
+
end
|
177
|
+
|
178
|
+
next false if paths.none? do |path|
|
179
|
+
path.end_with?('.swift', '.java', '.kt')
|
180
|
+
end
|
181
|
+
end
|
182
|
+
end
|
183
|
+
|
184
|
+
def to_s
|
185
|
+
<<~SUMMARY.yellow
|
186
|
+
|
187
|
+
Dead Code Analysis Results:
|
188
|
+
App ID: #{@metadata['app_id']}
|
189
|
+
App Version: #{@metadata['version']}
|
190
|
+
Platform: #{@metadata['platform']}
|
191
|
+
|
192
|
+
Statistics:
|
193
|
+
- Total User Sessions: #{@counts['user_sessions']}
|
194
|
+
- Seen Classes: #{@counts['seen_classes']}
|
195
|
+
- Unseen Classes: #{@counts['unseen_classes']}
|
196
|
+
SUMMARY
|
197
|
+
end
|
198
|
+
end
|
199
|
+
end
|
200
|
+
end
|
201
|
+
end
|
@@ -51,8 +51,8 @@ module EmergeCLI
|
|
51
51
|
success = false
|
52
52
|
|
53
53
|
begin
|
54
|
-
api_token = @options[:api_token] || ENV
|
55
|
-
raise 'API token is required'
|
54
|
+
api_token = @options[:api_token] || ENV.fetch('EMERGE_API_TOKEN', nil)
|
55
|
+
raise 'API token is required and cannot be blank' if api_token.nil? || api_token.strip.empty?
|
56
56
|
|
57
57
|
@network ||= EmergeCLI::Network.new(api_token:)
|
58
58
|
@git_info_provider ||= GitInfoProvider.new
|
@@ -133,8 +133,8 @@ module EmergeCLI
|
|
133
133
|
|
134
134
|
if seen_files[file_name]
|
135
135
|
Logger.warn "Duplicate file name detected: '#{file_name}'. " \
|
136
|
-
|
137
|
-
|
136
|
+
"Previous occurrence: '#{seen_files[file_name]}'. " \
|
137
|
+
'This upload will overwrite the previous one.'
|
138
138
|
end
|
139
139
|
seen_files[file_name] = image_path
|
140
140
|
end
|
data/lib/emerge_cli.rb
CHANGED
@@ -1,21 +1,28 @@
|
|
1
|
-
require_relative '
|
2
|
-
|
3
|
-
require_relative '
|
4
|
-
require_relative '
|
5
|
-
require_relative '
|
6
|
-
require_relative '
|
7
|
-
require_relative '
|
8
|
-
require_relative '
|
9
|
-
require_relative '
|
10
|
-
|
11
|
-
require_relative '
|
12
|
-
require_relative '
|
13
|
-
|
14
|
-
require_relative '
|
15
|
-
require_relative '
|
16
|
-
|
17
|
-
require_relative '
|
18
|
-
require_relative '
|
1
|
+
require_relative 'version'
|
2
|
+
|
3
|
+
require_relative 'commands/global_options'
|
4
|
+
require_relative 'commands/upload/snapshots/snapshots'
|
5
|
+
require_relative 'commands/upload/snapshots/client_libraries/swift_snapshot_testing'
|
6
|
+
require_relative 'commands/upload/snapshots/client_libraries/paparazzi'
|
7
|
+
require_relative 'commands/upload/snapshots/client_libraries/roborazzi'
|
8
|
+
require_relative 'commands/upload/snapshots/client_libraries/default'
|
9
|
+
require_relative 'commands/integrate/fastlane'
|
10
|
+
require_relative 'commands/config/snapshots/snapshots_ios'
|
11
|
+
require_relative 'commands/config/orderfiles/orderfiles_ios'
|
12
|
+
require_relative 'commands/reaper/reaper'
|
13
|
+
|
14
|
+
require_relative 'reaper/ast_parser'
|
15
|
+
require_relative 'reaper/code_deleter'
|
16
|
+
|
17
|
+
require_relative 'utils/git_info_provider'
|
18
|
+
require_relative 'utils/git_result'
|
19
|
+
require_relative 'utils/github'
|
20
|
+
require_relative 'utils/git'
|
21
|
+
require_relative 'utils/logger'
|
22
|
+
require_relative 'utils/network'
|
23
|
+
require_relative 'utils/profiler'
|
24
|
+
require_relative 'utils/project_detector'
|
25
|
+
require_relative 'utils/version_check'
|
19
26
|
|
20
27
|
require 'dry/cli'
|
21
28
|
|
@@ -34,7 +41,9 @@ module EmergeCLI
|
|
34
41
|
prefix.register 'snapshots-ios', Commands::Config::SnapshotsIOS
|
35
42
|
prefix.register 'order-files-ios', Commands::Config::OrderFilesIOS
|
36
43
|
end
|
44
|
+
|
45
|
+
register 'reaper', Commands::Reaper
|
37
46
|
end
|
38
47
|
|
39
48
|
# By default the log level is INFO, but can be overridden by the --debug flag
|
40
|
-
EmergeCLI::Logger.configure(
|
49
|
+
EmergeCLI::Logger.configure(Logger::INFO)
|