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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: f4bfa9e9a7d920f4f41ca06eef1283b73be375bffda3d414c27b55eeb11c1cef
4
- data.tar.gz: ed994835ee23a65f5ccfbec7acc84114f363f82f2464b4147939bb32b2664c3f
3
+ metadata.gz: f78e2be6724a6620a5135d20910b99fe76c8a959b00f6703bdad0c0d2c043742
4
+ data.tar.gz: 8366d036be25ec0b3da721346b1c71dcfcdccd2bf7b7f2bea9f3e6838cf62a8b
5
5
  SHA512:
6
- metadata.gz: de61137a2b6d33d3c6e3e8f8ab63b705658098d42c613a991f69102f01b93d3b374010939c087af06c8becfd20cb77c1f4279092ff5dc12556d803b607698a30
7
- data.tar.gz: bde1f8a729fd46b45600e8fcc50921399e334c1bf50b071c222bac4c81050a200819baeb643fbbab91fa1ef13c9b9dc5123258416e1c88fe2fd9cd284efb58f0
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.
@@ -18,8 +18,6 @@ module EmergeCLI
18
18
  ORDER_FILE = 'ORDER_FILE'.freeze
19
19
  ORDER_FILE_PATH = '$(PROJECT_DIR)/orderfiles/orderfile.txt'.freeze
20
20
 
21
- def initialize; end
22
-
23
21
  def call(**options)
24
22
  @options = options
25
23
  before(options)
@@ -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
- previews_exact.each do |preview|
209
- excluded.push({
210
- 'type' => 'exact',
211
- 'value' => preview
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({
@@ -9,6 +9,8 @@ module EmergeCLI
9
9
  def before(args)
10
10
  log_level = args[:debug] ? ::Logger::DEBUG : ::Logger::INFO
11
11
  EmergeCLI::Logger.configure(log_level)
12
+
13
+ EmergeCLI::Utils::VersionCheck.new.check_version
12
14
  end
13
15
  end
14
16
  end
@@ -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 = <<~'RUBY'.gsub(/^/, ' ')
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 = <<~'RUBY'.gsub(/^/, ' ')
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['EMERGE_API_TOKEN']
55
- raise 'API token is required' unless api_token
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
- "Previous occurrence: '#{seen_files[file_name]}'. " \
137
- 'This upload will overwrite the previous one.'
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 './commands/global_options'
2
- require_relative './commands/upload/snapshots/snapshots'
3
- require_relative './commands/upload/snapshots/client_libraries/swift_snapshot_testing'
4
- require_relative './commands/upload/snapshots/client_libraries/paparazzi'
5
- require_relative './commands/upload/snapshots/client_libraries/roborazzi'
6
- require_relative './commands/upload/snapshots/client_libraries/default'
7
- require_relative './commands/integrate/fastlane'
8
- require_relative './commands/config/snapshots/snapshots_ios'
9
- require_relative './commands/config/orderfiles/orderfiles_ios'
10
-
11
- require_relative './utils/git_info_provider'
12
- require_relative './utils/git_result'
13
- require_relative './utils/github'
14
- require_relative './utils/git'
15
- require_relative './utils/logger'
16
- require_relative './utils/network'
17
- require_relative './utils/profiler'
18
- require_relative './utils/project_detector'
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(::Logger::INFO)
49
+ EmergeCLI::Logger.configure(Logger::INFO)