diffend 0.2.27 → 0.2.32

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.
@@ -23,5 +23,7 @@ module Diffend
23
23
  RequestServerError = Class.new(BaseError)
24
24
  # Raised when we had an exception that we know how to handle
25
25
  HandledException = Class.new(BaseError)
26
+ # Raised when we are unable to resolve dependencies
27
+ DependenciesResolveException = Class.new(BaseError)
26
28
  end
27
29
  end
@@ -1,28 +1,38 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Diffend
4
- # Verifies voting verdicts for gems
5
- module Voting
4
+ # Executes a check for a given command
5
+ module Execute
6
6
  class << self
7
7
  # Build verdict
8
8
  #
9
- # @param command [String] either install or update
10
- # @param config [OpenStruct] diffend config
11
- # @param definition [Bundler::Definition] definition for your source
12
- def call(command, config, definition)
13
- Versions::Remote
14
- .call(command, config, definition)
15
- .tap { |response| build_message(command, config, response) }
9
+ # @param config [Diffend::Config]
10
+ def call(config)
11
+ Diffend::RequestVerdict
12
+ .call(config, build_definition(config.command))
13
+ .tap { |response| build_message(config, response) }
14
+ rescue Diffend::Errors::DependenciesResolveException
15
+ # We are unable to resolve dependencies, no message will be printed
16
16
  end
17
17
 
18
- # @param command [String] either install or update
19
- # @param config [OpenStruct] diffend config
18
+ # Build bundler definition
19
+ #
20
+ # @return [Bundler::Definition]
21
+ def build_definition(command)
22
+ Diffend::BuildBundlerDefinition.call(
23
+ command,
24
+ Bundler.default_gemfile,
25
+ Bundler.default_lockfile
26
+ )
27
+ end
28
+
29
+ # @param config [Diffend::Config]
20
30
  # @param response [Hash] response from diffend API
21
- def build_message(command, config, response)
31
+ def build_message(config, response)
22
32
  if response.key?('error')
23
- build_error(response)
33
+ build_error(config, response)
24
34
  elsif response.key?('action')
25
- build_verdict(command, config, response)
35
+ build_verdict(config, response)
26
36
  else
27
37
  Diffend::HandleErrors::Report.call(
28
38
  config: config,
@@ -36,25 +46,24 @@ module Diffend
36
46
  # @param response [Hash] response from diffend API
37
47
  def build_error(response)
38
48
  build_error_message(response)
39
- .tap(&Bundler.ui.method(:error))
49
+ .tap(&config.logger.method(:error))
40
50
 
41
51
  raise Diffend::Errors::HandledException
42
52
  end
43
53
 
44
- # @param command [String] either install or update
45
- # @param config [OpenStruct] diffend config
54
+ # @param config [Diffend::Config]
46
55
  # @param response [Hash] response from diffend API
47
- def build_verdict(command, config, response)
56
+ def build_verdict(config, response)
48
57
  case response['action']
49
58
  when 'allow'
50
- build_allow_message(command, response)
51
- .tap(&Bundler.ui.method(:confirm))
59
+ build_allow_message(config.command, response)
60
+ .tap(&config.logger.method(:info))
52
61
  when 'warn'
53
- build_warn_message(command, response)
54
- .tap(&Bundler.ui.method(:warn))
62
+ build_warn_message(config.command, response)
63
+ .tap(&config.logger.method(:warn))
55
64
  when 'deny'
56
- build_deny_message(command, response)
57
- .tap(&Bundler.ui.method(:error))
65
+ build_deny_message(config.command, response)
66
+ .tap(&config.logger.method(:error))
58
67
 
59
68
  exit 1
60
69
  else
@@ -77,7 +86,7 @@ module Diffend
77
86
  MSG
78
87
  end
79
88
 
80
- # @param command [String] either install or update
89
+ # @param command [String] command executed via bundler
81
90
  # @param response [Hash] response from diffend API
82
91
  #
83
92
  # @return [String]
@@ -89,7 +98,7 @@ module Diffend
89
98
  MSG
90
99
  end
91
100
 
92
- # @param command [String] either install or update
101
+ # @param command [String] command executed via bundler
93
102
  # @param response [Hash] response from diffend API
94
103
  #
95
104
  # @return [String]
@@ -101,7 +110,7 @@ module Diffend
101
110
  MSG
102
111
  end
103
112
 
104
- # @param command [String] either install or update
113
+ # @param command [String] command executed via bundler
105
114
  # @param response [Hash] response from diffend API
106
115
  #
107
116
  # @return [String]
@@ -114,7 +123,7 @@ module Diffend
114
123
  end
115
124
 
116
125
  # @param type [String] verdict type
117
- # @param command [String] either install or update
126
+ # @param command [String] command executed via bundler
118
127
  #
119
128
  # @return [String]
120
129
  def build_message_header(type, command)
@@ -7,10 +7,10 @@ module Diffend
7
7
  class << self
8
8
  # Execute request to Diffend
9
9
  #
10
+ # @param config [Diffend::Config]
11
+ # @param message [Symbol] message that we want to display
10
12
  # @param exception [Exception] expection that was raised
11
13
  # @param payload [Hash] with versions to check
12
- # @param config [OpenStruct] Diffend config
13
- # @param message [Symbol] message that we want to display
14
14
  # @param report [Boolean] if true we will report the issue to diffend
15
15
  # @param raise_exception [Boolean] if true we will raise an exception
16
16
  #
@@ -18,8 +18,11 @@ module Diffend
18
18
  def call(config:, message:, exception: nil, payload: {}, report: false, raise_exception: true)
19
19
  exception_payload = prepare_exception_payload(exception, payload)
20
20
 
21
- Bundler.ui.error(Diffend::HandleErrors::Messages::PAYLOAD_DUMP)
22
- Bundler.ui.error(Diffend::HandleErrors::Messages.const_get(message.to_s.upcase))
21
+ Diffend::HandleErrors::Messages::PAYLOAD_DUMP
22
+ .tap(&config.logger.method(:error))
23
+ Diffend::HandleErrors::Messages
24
+ .const_get(message.to_s.upcase)
25
+ .tap(&config.logger.method(:error))
23
26
 
24
27
  if report
25
28
  Diffend::Request.call(
@@ -30,14 +33,14 @@ module Diffend
30
33
  raise Diffend::Errors::HandledException if raise_exception
31
34
  end
32
35
 
33
- # @param config [OpenStruct] diffend config
36
+ # @param config [Diffend::Config]
34
37
  # @param payload [Hash]
35
38
  #
36
39
  # @return [Diffend::RequestObject]
37
40
  def build_request_object(config, payload)
38
41
  Diffend::RequestObject.new(
39
42
  config: config,
40
- url: errors_url(config.project_id),
43
+ url: config.errors_url,
41
44
  payload: payload,
42
45
  request_method: :post
43
46
  )
@@ -54,17 +57,6 @@ module Diffend
54
57
  .call(exception, payload)
55
58
  .tap(&Diffend::HandleErrors::DisplayToStdout.method(:call))
56
59
  end
57
-
58
- # Provides diffend errors endpoint url
59
- #
60
- # @param project_id [String] diffend project_id
61
- #
62
- # @return [String] diffend endpoint
63
- def errors_url(project_id)
64
- return ENV['DIFFEND_ERROR_URL'] if ENV.key?('DIFFEND_ERROR_URL')
65
-
66
- "https://my.diffend.io/api/projects/#{project_id}/errors"
67
- end
68
60
  end
69
61
  end
70
62
  end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Verify if we are running latest version of the plugin
5
+ module LatestVersion
6
+ class << self
7
+ # Verify if we are running latest version of the plugin
8
+ #
9
+ # @param config [Diffend::Config]
10
+ def call(config)
11
+ return if config.development?
12
+ return if installed_version == Diffend::VERSION
13
+
14
+ print_message(config, installed_version)
15
+
16
+ exit 2
17
+ end
18
+
19
+ private
20
+
21
+ # @return [String] installed plugin version
22
+ def installed_version
23
+ ::Bundler::Plugin
24
+ .index
25
+ .plugin_path('diffend')
26
+ .basename
27
+ .to_s
28
+ .split('-')
29
+ .last
30
+ end
31
+
32
+ # @param config [Diffend::Config]
33
+ # @param version [Hash] installed version
34
+ def print_message(config, version)
35
+ build_message(version)
36
+ .tap(&config.logger.method(:error))
37
+ end
38
+
39
+ # @param version [Hash] installed version
40
+ #
41
+ # @return [String]
42
+ def build_message(version)
43
+ <<~MSG
44
+ \nYou are running an outdated version (#{version}) of the plugin, which will lead to issues.
45
+ \nPlease upgrade to the latest one (#{Diffend::VERSION}) by executing "rm -rf .bundle/plugin".\n
46
+ MSG
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Module responsible for building local context
5
+ module LocalContext
6
+ class << self
7
+ # Build diffend, host, packages, and platform specific information
8
+ #
9
+ # @param config [Diffend::Config]
10
+ # @param definition [Bundler::Definition] definition for your source
11
+ #
12
+ # @return [Hash] payload for diffend endpoint
13
+ def call(config, definition)
14
+ {
15
+ 'diffend' => Diffend.call(config),
16
+ 'host' => Host.call,
17
+ 'packages' => Packages.call(config.command, definition),
18
+ 'platform' => Platform.call
19
+ }.freeze
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Module responsible for building local context
5
+ module LocalContext
6
+ # Module responsible for building diffend information from local context
7
+ module Diffend
8
+ # API version
9
+ API_VERSION = '0.1'
10
+ # Platform type ruby
11
+ PLATFORM_TYPE = 0
12
+
13
+ private_constant :API_VERSION, :PLATFORM_TYPE
14
+
15
+ class << self
16
+ # Build diffend information
17
+ #
18
+ # @param config [Diffend::Config]
19
+ #
20
+ # @return [Hash]
21
+ def call(config)
22
+ {
23
+ 'api_version' => API_VERSION,
24
+ 'environment' => config.env,
25
+ 'project_id' => config.project_id,
26
+ 'type' => PLATFORM_TYPE,
27
+ 'version' => ::Diffend::VERSION
28
+ }.freeze
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,88 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'etc'
4
+
5
+ module Diffend
6
+ # Module responsible for building local context
7
+ module LocalContext
8
+ # Module responsible for building host information from local context
9
+ module Host
10
+ class << self
11
+ # Build host information
12
+ #
13
+ # @return [Hash]
14
+ def call
15
+ uname = Etc.uname
16
+
17
+ {
18
+ 'command' => command,
19
+ 'ips' => ips,
20
+ 'name' => uname[:nodename],
21
+ 'system' => {
22
+ 'machine' => uname[:machine],
23
+ 'name' => uname[:sysname],
24
+ 'release' => uname[:release],
25
+ 'version' => uname[:version]
26
+ },
27
+ 'tags' => tags,
28
+ 'user' => Etc.getpwuid(Process.uid).name,
29
+ 'pid' => Process.pid
30
+ }.freeze
31
+ end
32
+
33
+ private
34
+
35
+ # Build host command information
36
+ #
37
+ # @return [Hash]
38
+ def command
39
+ if File.exist?($PROGRAM_NAME)
40
+ if defined?(JRUBY_VERSION)
41
+ name = $PROGRAM_NAME.split('/').last.strip
42
+ command = "#{name} #{ARGV.join(' ')}"
43
+ else
44
+ array = `ps -p #{Process.pid} -o command=`.strip.split(' ')
45
+ array.shift if array.first.end_with?('bin/ruby')
46
+ name = array.shift.split('/').last.strip
47
+ command = "#{name} #{array.join(' ')}"
48
+ end
49
+
50
+ { 'name' => command, 'title' => '' }
51
+ else
52
+ { 'name' => ARGV.join(' '), 'title' => $PROGRAM_NAME }
53
+ end
54
+ end
55
+
56
+ # Build host ips, except localhost and loopback
57
+ #
58
+ # @return [Array<String>]
59
+ def ips
60
+ Socket.ip_address_list.map do |ip|
61
+ next if ip.ipv4_loopback? || ip.ipv6_loopback? || ip.ipv6_linklocal?
62
+
63
+ ip.ip_address
64
+ end.compact
65
+ end
66
+
67
+ # Build host tags
68
+ #
69
+ # @return [Array]
70
+ def tags
71
+ tags = []
72
+
73
+ if ENV.key?('GITHUB_ACTIONS')
74
+ tags << 'ci'
75
+ tags << 'ci-github'
76
+ end
77
+
78
+ if ENV.key?('CIRCLECI')
79
+ tags << 'ci'
80
+ tags << 'ci-circle'
81
+ end
82
+
83
+ tags
84
+ end
85
+ end
86
+ end
87
+ end
88
+ end
@@ -0,0 +1,302 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Diffend
4
+ # Module responsible for building local context
5
+ module LocalContext
6
+ # Module responsible for building packages information from local context
7
+ class Packages
8
+ # Definition of a local path, if it matches it means that we are the source
9
+ ME_PATH = '.'
10
+ # Sources that we expect to match ourselves too
11
+ ME_SOURCES = [
12
+ Bundler::Source::Gemspec,
13
+ Bundler::Source::Path
14
+ ].freeze
15
+ # List of dependency types
16
+ DEPENDENCIES_TYPES = {
17
+ direct: 0,
18
+ dependency: 1
19
+ }.freeze
20
+ # List of sources types
21
+ SOURCES_TYPES = {
22
+ valid: 0,
23
+ multiple_primary: 1
24
+ }.freeze
25
+ # List of gem sources types
26
+ GEM_SOURCES_TYPES = {
27
+ local: 0,
28
+ gemfile_source: 1,
29
+ gemfile_git: 2,
30
+ gemfile_path: 3
31
+ }.freeze
32
+
33
+ class << self
34
+ # @param command [String] command executed via bundler
35
+ # @param definition [Bundler::Definition] definition for your source
36
+ def call(command, definition)
37
+ Bundler.ui.silence { definition.resolve_remotely! }
38
+
39
+ instance = new(definition)
40
+
41
+ case command
42
+ when Commands::INSTALL, Commands::EXEC then instance.build_install
43
+ when Commands::UPDATE then instance.build_update
44
+ else
45
+ raise ArgumentError, "invalid command: #{command}"
46
+ end
47
+ end
48
+ end
49
+
50
+ # @param definition [Bundler::Definition] definition for your source
51
+ #
52
+ # @return [Hash] local dependencies
53
+ def initialize(definition)
54
+ @definition = definition
55
+ @direct_dependencies = Hash[definition.dependencies.map { |val| [val.name, val] }]
56
+ # Support case without Gemfile.lock
57
+ @locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : []
58
+ end
59
+
60
+ # Build install specification
61
+ #
62
+ # @return [Hash]
63
+ def build_install
64
+ hash = build_main
65
+
66
+ @definition.specs.each do |spec|
67
+ next if skip?(spec.source)
68
+
69
+ locked_spec = @locked_specs.find { |s| s.name == spec.name }
70
+
71
+ hash['dependencies'][spec.name] = {
72
+ 'platform' => build_spec_platform(spec, locked_spec),
73
+ 'source' => build_spec_source(spec),
74
+ 'type' => build_dependency_type(spec.name),
75
+ 'versions' => build_versions(spec, locked_spec)
76
+ }
77
+ end
78
+
79
+ hash
80
+ end
81
+
82
+ # Build update specification
83
+ #
84
+ # @return [Hash]
85
+ def build_update
86
+ hash = build_main
87
+
88
+ @definition.specs.each do |spec|
89
+ next if skip?(spec.source)
90
+
91
+ locked_spec = @locked_specs.find { |s| s.name == spec.name }
92
+
93
+ hash['dependencies'][spec.name] = {
94
+ 'platform' => build_spec_platform(spec, locked_spec),
95
+ 'source' => build_spec_source(spec),
96
+ 'type' => build_dependency_type(spec.name),
97
+ 'versions' => build_versions(spec, locked_spec)
98
+ }
99
+ end
100
+
101
+ hash
102
+ end
103
+
104
+ private
105
+
106
+ # Build default specification
107
+ #
108
+ # @return [Hash]
109
+ def build_main
110
+ {
111
+ 'dependencies' => {},
112
+ 'sources' => build_sources,
113
+ 'plugins' => {},
114
+ 'platforms' => @definition.platforms.map(&:to_s)
115
+ }
116
+ end
117
+
118
+ # Build gem versions
119
+ #
120
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
121
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
122
+ #
123
+ # @return [Array<String>]
124
+ def build_versions(spec, locked_spec = nil)
125
+ if locked_spec && locked_spec.version.to_s != spec.version.to_s
126
+ [locked_spec.version.to_s, spec.version.to_s]
127
+ else
128
+ [spec.version.to_s]
129
+ end
130
+ end
131
+
132
+ # @param specs [Array] specs that are direct dependencies
133
+ # @param name [String] spec name
134
+ #
135
+ # @return [Boolean] dependency type
136
+ def build_dependency_type(name)
137
+ if @direct_dependencies.key?(name)
138
+ DEPENDENCIES_TYPES[:direct]
139
+ else
140
+ DEPENDENCIES_TYPES[:dependency]
141
+ end
142
+ end
143
+
144
+ # Build gem platform
145
+ #
146
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
147
+ # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass]
148
+ #
149
+ # @return [String]
150
+ def build_spec_platform(spec, locked_spec)
151
+ parse_platform(
152
+ spec.platform || locked_spec&.platform || spec.send(:generic_local_platform)
153
+ )
154
+ end
155
+
156
+ # Parse gem platform
157
+ #
158
+ # @param platform [String, Gem::Platform]
159
+ #
160
+ # @return [String]
161
+ def parse_platform(platform)
162
+ case platform
163
+ when String then platform
164
+ when Gem::Platform then platform.to_s
165
+ end
166
+ end
167
+
168
+ # Build gem source type
169
+ #
170
+ # @param source [Bundler::Source] gem source type
171
+ #
172
+ # @return [Integer] internal gem source type
173
+ def build_spec_gem_source_type(source)
174
+ case source
175
+ when Bundler::Source::Metadata
176
+ GEM_SOURCES_TYPES[:local]
177
+ when Bundler::Source::Rubygems, Bundler::Source::Rubygems::Remote
178
+ GEM_SOURCES_TYPES[:gemfile_source]
179
+ when Bundler::Source::Git
180
+ GEM_SOURCES_TYPES[:gemfile_git]
181
+ when Bundler::Source::Path
182
+ GEM_SOURCES_TYPES[:gemfile_path]
183
+ else
184
+ raise ArgumentError, "unknown source #{source.class}"
185
+ end
186
+ end
187
+
188
+ # Build gem source
189
+ #
190
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
191
+ #
192
+ # @return [Hash]
193
+ def build_spec_source(spec)
194
+ source = source_for_spec(spec)
195
+
196
+ {
197
+ 'type' => build_spec_gem_source_type(source),
198
+ 'value' => source_name_from_source(source)
199
+ }
200
+ end
201
+
202
+ # Figure out source for gem
203
+ #
204
+ # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification]
205
+ #
206
+ # @return [Bundler::Source] gem source type
207
+ def source_for_spec(spec)
208
+ return spec.remote if spec.remote
209
+
210
+ case spec.source
211
+ when Bundler::Source::Rubygems
212
+ spec
213
+ .source
214
+ .send(:remote_specs)
215
+ .search(Bundler::Dependency.new(spec.name, spec.version))
216
+ .last
217
+ .remote
218
+ when Bundler::Source::Metadata, Bundler::Source::Git, Bundler::Source::Path
219
+ spec.source
220
+ else
221
+ raise ArgumentError, "unknown source #{spec.source.class}"
222
+ end
223
+ end
224
+
225
+ # Build gem source name
226
+ #
227
+ # @param source [Bundler::Source] gem source type
228
+ #
229
+ # @return [String]
230
+ def source_name_from_source(source)
231
+ case source
232
+ when Bundler::Source::Metadata
233
+ ''
234
+ when Bundler::Source::Rubygems::Remote
235
+ source_name(source.anonymized_uri)
236
+ when Bundler::Source::Git
237
+ source.instance_variable_get(:@safe_uri)
238
+ when Bundler::Source::Path
239
+ source.path
240
+ else
241
+ raise ArgumentError, "unknown source #{source.class}"
242
+ end
243
+ end
244
+
245
+ # @param uri [Bundler::URI]
246
+ #
247
+ # @return [String]
248
+ def source_name(uri)
249
+ uri.to_s[0...-1]
250
+ end
251
+
252
+ # Build sources used in the Gemfile
253
+ #
254
+ # @return [Array<Hash>]
255
+ def build_sources
256
+ sources = @definition.send(:sources).rubygems_sources
257
+ hash = {}
258
+
259
+ sources.each do |source|
260
+ type = build_source_type(source.remotes)
261
+
262
+ source.remotes.each do |src|
263
+ hash[source_name(src)] = type
264
+ end
265
+ end
266
+
267
+ hash.map { |name, type| { 'name' => name, 'type' => type } }
268
+ end
269
+
270
+ # Build gem source type
271
+ #
272
+ # @param remotes [Array<Bundler::URI>]
273
+ #
274
+ # @return [Integer] internal source type
275
+ def build_source_type(remotes)
276
+ remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid]
277
+ end
278
+
279
+ # Checks if we should skip a source
280
+ #
281
+ # @param source [Bundler::Source] gem source type
282
+ #
283
+ # @return [Boolean] true if we should skip this source, false otherwise
284
+ def skip?(source)
285
+ return true if me?(source)
286
+
287
+ false
288
+ end
289
+
290
+ # Checks if it's a self source, this happens for repositories that are a gem
291
+ #
292
+ # @param source [Bundler::Source] gem source type
293
+ #
294
+ # @return [Boolean] true if it's a self source, false otherwise
295
+ def me?(source)
296
+ return false unless ME_SOURCES.include?(source.class)
297
+
298
+ source.path.to_s == ME_PATH
299
+ end
300
+ end
301
+ end
302
+ end