# frozen_string_literal: true module Diffend module Voting # Module responsible for handling both local and remote gem versions module Versions # Module responsible for preparing current or current/new versions of gems class Local # Definition of a local path, if it matches it means that we are the source ME_PATH = '.' # Sources that we expect to match ourselves too ME_SOURCES = [ Bundler::Source::Gemspec, Bundler::Source::Path ].freeze # List of dependency types DEPENDENCIES_TYPES = { direct: 0, dependency: 1 }.freeze # List of sources types SOURCES_TYPES = { valid: 0, multiple_primary: 1 }.freeze # List of gem sources types GEM_SOURCES_TYPES = { local: 0, gemfile_source: 1, gemfile_git: 2, gemfile_path: 3 }.freeze class << self # @param command [String] either install or update # @param definition [Bundler::Definition] definition for your source def call(command, definition) Bundler.ui.silence { definition.resolve_remotely! } instance = new(definition) case command when Commands::INSTALL then instance.build_install when Commands::UPDATE then instance.build_update else raise ArgumentError, "invalid command: #{command}" end end end # @param definition [Bundler::Definition] definition for your source # # @return [Hash] local dependencies def initialize(definition) @definition = definition @direct_dependencies = Hash[definition.dependencies.map { |val| [val.name, val] }] @main_source = definition.send(:sources).rubygems_sources.last # Support case without Gemfile.lock @locked_specs = @definition.locked_gems ? @definition.locked_gems.specs : [] end # Build install specification # # @return [Hash] def build_install hash = build_main @definition.specs.each do |spec| next if skip?(spec.source) locked_spec = @locked_specs.find { |s| s.name == spec.name } hash['dependencies'][spec.name] = { 'platform' => build_spec_platform(spec, locked_spec), 'source' => build_spec_source(spec), 'type' => build_dependency_type(spec.name), 'versions' => build_versions(spec, locked_spec) } end hash end # Build update specification # # @return [Hash] def build_update hash = build_main @definition.specs.each do |spec| next if skip?(spec.source) locked_spec = @locked_specs.find { |s| s.name == spec.name } hash['dependencies'][spec.name] = { 'platform' => build_spec_platform(spec, locked_spec), 'source' => build_spec_source(spec), 'type' => build_dependency_type(spec.name), 'versions' => build_versions(spec, locked_spec) } end hash end private # Build default specification # # @return [Hash] def build_main { 'dependencies' => {}, 'sources' => build_sources, 'plugins' => {}, 'platforms' => @definition.platforms.map(&:to_s) } end # Build gem versions # # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification] # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass] # # @return [Array] def build_versions(spec, locked_spec = nil) if locked_spec && locked_spec.version.to_s != spec.version.to_s [locked_spec.version.to_s, spec.version.to_s] else [spec.version.to_s] end end # @param specs [Array] specs that are direct dependencies # @param name [String] spec name # # @return [Boolean] dependency type def build_dependency_type(name) if @direct_dependencies.key?(name) DEPENDENCIES_TYPES[:direct] else DEPENDENCIES_TYPES[:dependency] end end # Build gem platform # # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification] # @param locked_spec [Bundler::LazySpecification, Gem::Specification, NilClass] # # @return [String] def build_spec_platform(spec, locked_spec) spec.platform || locked_spec&.platform || build_spec_generic_platform(spec) end # Build gem generic platform # # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification] # # @return [String] def build_spec_generic_platform(spec) platform = spec.send(:generic_local_platform) case platform when String then platform when Gem::Platform then platform.os end end # Build gem source type # # @param source [Bundler::Source] gem source type # # @return [Integer] internal gem source type def build_spec_gem_source_type(source) case source when Bundler::Source::Metadata GEM_SOURCES_TYPES[:local] when Bundler::Source::Rubygems GEM_SOURCES_TYPES[:gemfile_source] when Bundler::Source::Git GEM_SOURCES_TYPES[:gemfile_git] when Bundler::Source::Path GEM_SOURCES_TYPES[:gemfile_path] else raise ArgumentError, "unknown source #{source.class}" end end # Build gem source # # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification] # # @return [Hash] def build_spec_source(spec) source = source_for_spec(spec) { 'type' => build_spec_gem_source_type(source), 'value' => source_name_from_source(source) } end # Figure out source for gem # # @param spec [Bundler::StubSpecification, Bundler::LazySpecification, Gem::Specification] # # @return [Bundler::Source] gem source type def source_for_spec(spec) if @direct_dependencies.key?(spec.name) @direct_dependencies[spec.name].source || @main_source else spec.source end end # Build gem source name # # @param source [Bundler::Source] gem source type # # @return [String] def source_name_from_source(source) case source when Bundler::Source::Metadata '' when Bundler::Source::Rubygems source_name(source.remotes.first) when Bundler::Source::Git source.uri when Bundler::Source::Path source.path else raise ArgumentError, "unknown source #{source.class}" end end # @param name [Bundler::URI] # # @return [String] cleaned source name def source_name(name) name.to_s[0...-1] end # Build sources used in the Gemfile # # @return [Array] def build_sources sources = @definition.send(:sources).rubygems_sources hash = {} sources.each do |source| type = build_source_type(source.remotes) source.remotes.each do |src| hash[source_name(src)] = type end end hash.map { |name, type| { 'name' => name, 'type' => type } } end # Build gem source type # # @param remotes [Array] # # @return [Integer] internal source type def build_source_type(remotes) remotes.count > 1 ? SOURCES_TYPES[:multiple_primary] : SOURCES_TYPES[:valid] end # Checks if we should skip a source # # @param source [Bundler::Source] gem source type # # @return [Boolean] true if we should skip this source, false otherwise def skip?(source) return true if me?(source) false end # Checks if it's a self source, this happens for repositories that are a gem # # @param source [Bundler::Source] gem source type # # @return [Boolean] true if it's a self source, false otherwise def me?(source) return false unless ME_SOURCES.include?(source.class) source.path.to_s == ME_PATH end end end end end