[rubygems/rubygems] Use a constant empty tar header to avoid extra allocations
[ruby.git] / lib / bundler / index.rb
blobdf46facc88ab710913c81edc60c5acc89ce7ca00
1 # frozen_string_literal: true
3 module Bundler
4   class Index
5     include Enumerable
7     def self.build
8       i = new
9       yield i
10       i
11     end
13     attr_reader :specs, :duplicates, :sources
14     protected :specs, :duplicates
16     RUBY = "ruby"
17     NULL = "\0"
19     def initialize
20       @sources = []
21       @cache = {}
22       @specs = {}
23       @duplicates = {}
24     end
26     def initialize_copy(o)
27       @sources = o.sources.dup
28       @cache = {}
29       @specs = {}
30       @duplicates = {}
32       o.specs.each do |name, hash|
33         @specs[name] = hash.dup
34       end
35       o.duplicates.each do |name, array|
36         @duplicates[name] = array.dup
37       end
38     end
40     def inspect
41       "#<#{self.class}:0x#{object_id} sources=#{sources.map(&:inspect)} specs.size=#{specs.size}>"
42     end
44     def empty?
45       each { return false }
46       true
47     end
49     def search_all(name, &blk)
50       return enum_for(:search_all, name) unless blk
51       specs_by_name(name).each(&blk)
52       @duplicates[name]&.each(&blk)
53       @sources.each {|source| source.search_all(name, &blk) }
54     end
56     # Search this index's specs, and any source indexes that this index knows
57     # about, returning all of the results.
58     def search(query)
59       results = local_search(query)
60       return results unless @sources.any?
62       @sources.each do |source|
63         results = safe_concat(results, source.search(query))
64       end
65       results.uniq!(&:full_name) unless results.empty? # avoid modifying frozen EMPTY_SEARCH
66       results
67     end
69     alias_method :[], :search
71     def local_search(query)
72       case query
73       when Gem::Specification, RemoteSpecification, LazySpecification, EndpointSpecification then search_by_spec(query)
74       when String then specs_by_name(query)
75       when Array then specs_by_name_and_version(*query)
76       else
77         raise "You can't search for a #{query.inspect}."
78       end
79     end
81     def add(spec)
82       (@specs[spec.name] ||= {}).store(spec.full_name, spec)
83     end
84     alias_method :<<, :add
86     def each(&blk)
87       return enum_for(:each) unless blk
88       specs.values.each do |spec_sets|
89         spec_sets.values.each(&blk)
90       end
91       sources.each {|s| s.each(&blk) }
92       self
93     end
95     def spec_names
96       names = specs.keys + sources.map(&:spec_names)
97       names.uniq!
98       names
99     end
101     def unmet_dependency_names
102       dependency_names.select do |name|
103         search(name).empty?
104       end
105     end
107     def dependency_names
108       names = []
109       each do |spec|
110         spec.dependencies.each do |dep|
111           next if dep.type == :development
112           names << dep.name
113         end
114       end
115       names.uniq
116     end
118     # Combines indexes proritizing existing specs, like `Hash#reverse_merge!`
119     # Duplicate specs found in `other` are stored in `@duplicates`.
120     def use(other)
121       return unless other
122       other.each do |spec|
123         exist?(spec) ? add_duplicate(spec) : add(spec)
124       end
125       self
126     end
128     # Combines indexes proritizing specs from `other`, like `Hash#merge!`
129     # Duplicate specs found in `self` are saved in `@duplicates`.
130     def merge!(other)
131       return unless other
132       other.each do |spec|
133         if existing = find_by_spec(spec)
134           add_duplicate(existing)
135         end
136         add spec
137       end
138       self
139     end
141     def size
142       @sources.inject(@specs.size) do |size, source|
143         size += source.size
144       end
145     end
147     # Whether all the specs in self are in other
148     def subset?(other)
149       all? do |spec|
150         other_spec = other[spec].first
151         other_spec && dependencies_eql?(spec, other_spec) && spec.source == other_spec.source
152       end
153     end
155     def dependencies_eql?(spec, other_spec)
156       deps       = spec.dependencies.select {|d| d.type != :development }
157       other_deps = other_spec.dependencies.select {|d| d.type != :development }
158       deps.sort == other_deps.sort
159     end
161     def add_source(index)
162       raise ArgumentError, "Source must be an index, not #{index.class}" unless index.is_a?(Index)
163       @sources << index
164       @sources.uniq! # need to use uniq! here instead of checking for the item before adding
165     end
167     private
169     def safe_concat(a, b)
170       return a if b.empty?
171       return b if a.empty?
172       a.concat(b)
173     end
175     def add_duplicate(spec)
176       (@duplicates[spec.name] ||= []) << spec
177     end
179     def specs_by_name_and_version(name, version)
180       results = @specs[name]&.values
181       return EMPTY_SEARCH unless results
182       results.select! {|spec| spec.version == version }
183       results
184     end
186     def specs_by_name(name)
187       @specs[name]&.values || EMPTY_SEARCH
188     end
190     EMPTY_SEARCH = [].freeze
192     def search_by_spec(spec)
193       spec = find_by_spec(spec)
194       spec ? [spec] : EMPTY_SEARCH
195     end
197     def find_by_spec(spec)
198       @specs[spec.name]&.fetch(spec.full_name, nil)
199     end
201     def exist?(spec)
202       @specs[spec.name]&.key?(spec.full_name)
203     end
204   end