-
Notifications
You must be signed in to change notification settings - Fork 350
/
source.rb
476 lines (422 loc) · 14 KB
/
source.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
require 'cocoapods-core/source/acceptor'
require 'cocoapods-core/source/aggregate'
require 'cocoapods-core/source/health_reporter'
require 'cocoapods-core/source/manager'
require 'cocoapods-core/source/metadata'
module Pod
# The Source class is responsible to manage a collection of podspecs.
#
# The backing store of the podspecs collection is an implementation detail
# abstracted from the rest of CocoaPods.
#
# The default implementation uses a git repo as a backing store, where the
# podspecs are namespaced as:
#
# "#{SPEC_NAME}/#{VERSION}/#{SPEC_NAME}.podspec"
#
class Source
# The default branch in which the specs are stored
DEFAULT_SPECS_BRANCH = 'master'.freeze
# @return [Pod::Source::Metadata] The metadata for this source.
#
attr_reader :metadata
# @param [Pathname, String] repo @see #repo.
#
def initialize(repo)
@repo = Pathname(repo).expand_path
@versions_by_name = {}
refresh_metadata
end
# @return [String] The name of the source.
#
def name
repo.basename.to_s
end
# @return [String] The URL of the source.
#
# @note In the past we had used `git ls-remote --get-url`, but this could
# lead to an issue when finding a source based on its URL when `git`
# is configured to rewrite URLs with the `url.<base>.insteadOf`
# option. See https://github.com/CocoaPods/CocoaPods/issues/2724.
#
def url
@url ||= begin
remote = repo_git(%w(config --get remote.origin.url))
if !remote.empty?
remote
elsif (repo + '.git').exist?
"file://#{repo}/.git"
end
end
end
# @return [String] The type of the source.
#
def type
git? ? 'git' : 'file system'
end
alias_method :to_s, :name
# @return [Integer] compares a source with another one for sorting
# purposes.
#
# @note Source are compared by the alphabetical order of their name, and
# this convention should be used in any case where sources need to
# be disambiguated.
#
def <=>(other)
name <=> other.name
end
# @return [String] A description suitable for debugging.
#
def inspect
"#<#{self.class} name:#{name} type:#{type}>"
end
# @!group Paths
#-------------------------------------------------------------------------#
# @return [Pathname] The path where the source is stored.
#
attr_reader :repo
# @return [Pathname] The directory where the specs are stored.
#
# @note In previous versions of CocoaPods they used to be stored in
# the root of the repo. This lead to issues, especially with
# the GitHub interface and now they are stored in a dedicated
# folder.
#
def specs_dir
@specs_dir ||= begin
specs_sub_dir = repo + 'Specs'
if specs_sub_dir.exist?
specs_sub_dir
elsif repo.exist?
repo
end
end
end
# @param [String] name The name of the pod.
#
# @return [Pathname] The path at which the specs for the given pod are
# stored.
#
def pod_path(name)
specs_dir.join(*metadata.path_fragment(name))
end
# @return [Pathname] The path at which source metadata is stored.
#
def metadata_path
repo + 'CocoaPods-version.yml'
end
public
# @!group Querying the source
#-------------------------------------------------------------------------#
# @return [Array<String>] the list of the name of all the Pods.
#
#
def pods
unless specs_dir
raise Informative, "Unable to find a source named: `#{name}`"
end
glob = specs_dir.join('*/' * metadata.prefix_lengths.size, '*')
Pathname.glob(glob).reduce([]) do |pods, entry|
pods << entry.basename.to_s if entry.directory?
pods
end.sort
end
# Returns pod names for given array of specification paths.
#
# @param [Array<String>] spec_paths
# Array of file path names for specifications. Path strings should be relative to the source path.
#
# @return [Array<String>] the list of the name of Pods corresponding to specification paths.
#
def pods_for_specification_paths(spec_paths)
spec_paths.map do |path|
absolute_path = repo + path
relative_path = absolute_path.relative_path_from(specs_dir)
# The first file name returned by 'each_filename' is the pod name
relative_path.each_filename.first
end
end
# @return [Array<Version>] all the available versions for the Pod, sorted
# from highest to lowest.
#
# @param [String] name
# the name of the Pod.
#
def versions(name)
return nil unless specs_dir
raise ArgumentError, 'No name' unless name
pod_dir = pod_path(name)
return unless pod_dir.exist?
@versions_by_name[name] ||= pod_dir.children.map do |v|
next nil unless v.directory?
basename = v.basename.to_s
next unless basename[0, 1] != '.'
begin
Version.new(basename)
rescue ArgumentError
raise Informative, 'An unexpected version directory ' \
"`#{basename}` was encountered for the " \
"`#{pod_dir}` Pod in the `#{name}` repository."
end
end.compact.sort.reverse
end
# @return [Specification] the specification for a given version of Pod.
#
# @param @see specification_path
#
def specification(name, version)
Specification.from_file(specification_path(name, version))
end
# Returns the path of the specification with the given name and version.
#
# @param [String] name
# the name of the Pod.
#
# @param [Version,String] version
# the version for the specification.
#
# @return [Pathname] The path of the specification.
#
def specification_path(name, version)
raise ArgumentError, 'No name' unless name
raise ArgumentError, 'No version' unless version
path = pod_path(name) + version.to_s
specification_path = path + "#{name}.podspec.json"
unless specification_path.exist?
specification_path = path + "#{name}.podspec"
end
unless specification_path.exist?
raise StandardError, "Unable to find the specification #{name} " \
"(#{version}) in the #{self.name} source."
end
specification_path
end
# @return [Array<Specification>] all the specifications contained by the
# source.
#
def all_specs
glob = specs_dir.join('*/' * metadata.prefix_lengths.size, '*', '*', '*.podspec{.json,}')
specs = Pathname.glob(glob).map do |path|
begin
Specification.from_file(path)
rescue
CoreUI.warn "Skipping `#{path.relative_path_from(repo)}` because the " \
'podspec contains errors.'
next
end
end
specs.compact
end
# Returns the set for the Pod with the given name.
#
# @param [String] pod_name
# The name of the Pod.
#
# @return [Sets] the set.
#
def set(pod_name)
Specification::Set.new(pod_name, self)
end
# @return [Array<Sets>] the sets of all the Pods.
#
def pod_sets
pods.map { |pod_name| set(pod_name) }
end
public
# @!group Searching the source
#-------------------------------------------------------------------------#
# @return [Set] a set for a given dependency. The set is identified by the
# name of the dependency and takes into account subspecs.
#
# @note This method is optimized for fast lookups by name, i.e. it does
# *not* require iterating through {#pod_sets}
#
# @todo Rename to #load_set
#
def search(query)
unless specs_dir
raise Informative, "Unable to find a source named: `#{name}`"
end
if query.is_a?(Dependency)
query = query.root_name
end
if (versions = @versions_by_name[query]) && !versions.empty?
set = set(query)
return set if set.specification_name == query
end
found = []
Pathname.glob(pod_path(query)) do |path|
next unless Dir.foreach(path).any? { |child| child != '.' && child != '..' }
found << path.basename.to_s
end
if [query] == found
set = set(query)
set if set.specification_name == query
end
end
# @return [Array<Set>] The list of the sets that contain the search term.
#
# @param [String] query
# the search term. Can be a regular expression.
#
# @param [Boolean] full_text_search
# whether the search should be limited to the name of the Pod or
# should include also the author, the summary, and the description.
#
# @note full text search requires to load the specification for each pod,
# hence is considerably slower.
#
# @todo Rename to #search
#
def search_by_name(query, full_text_search = false)
regexp_query = /#{query}/i
if full_text_search
pod_sets.reject do |set|
texts = []
begin
s = set.specification
texts << s.name
texts += s.authors.keys
texts << s.summary
texts << s.description
rescue
CoreUI.warn "Skipping `#{set.name}` because the podspec " \
'contains errors.'
end
texts.grep(regexp_query).empty?
end
else
names = pods.grep(regexp_query)
names.map { |pod_name| set(pod_name) }
end
end
# Returns the set of the Pod whose name fuzzily matches the given query.
#
# @param [String] query
# The query to search for.
#
# @return [Set] The name of the Pod.
# @return [Nil] If no Pod with a suitable name was found.
#
def fuzzy_search(query)
require 'fuzzy_match'
pod_name = FuzzyMatch.new(pods).find(query)
if pod_name
search(pod_name)
end
end
# @!group Updating the source
#-------------------------------------------------------------------------#
# Updates the local clone of the source repo.
#
# @param [Boolean] show_output
#
# @return [Array<String>] changed_spec_paths
# Returns the list of changed spec paths.
#
def update(show_output)
return [] if unchanged_github_repo?
prev_commit_hash = git_commit_hash
update_git_repo(show_output)
@versions_by_name.clear
refresh_metadata
if version = metadata.last_compatible_version(Version.new(CORE_VERSION))
tag = "v#{version}"
CoreUI.warn "Using the `#{tag}` tag of the `#{name}` source because " \
"it is the last version compatible with CocoaPods #{CORE_VERSION}."
repo_git(['checkout', tag])
end
diff_until_commit_hash(prev_commit_hash)
end
def updateable?
git?
end
def git?
repo.join('.git').exist? && !repo_git(%w(rev-parse HEAD)).empty?
end
def indexable?
true
end
def verify_compatibility!
return if metadata.compatible?(CORE_VERSION)
version_msg = if metadata.minimum_cocoapods_version == metadata.maximum_cocoapods_version
metadata.minimum_cocoapods_version
else
"#{metadata.minimum_cocoapods_version} - #{metadata.maximum_cocoapods_version}"
end
raise Informative, "The `#{name}` repo requires " \
"CocoaPods #{version_msg} (currently using #{CORE_VERSION})\n" \
'Update CocoaPods, or checkout the appropriate tag in the repo.'
end
public
# @!group Representations
#-------------------------------------------------------------------------#
# @return [Hash{String=>{String=>Specification}}] the static representation
# of all the specifications grouped first by name and then by
# version.
#
def to_hash
hash = {}
all_specs.each do |spec|
hash[spec.name] ||= {}
hash[spec.name][spec.version.version] = spec.to_hash
end
hash
end
# @return [String] the YAML encoded {to_hash} representation.
#
def to_yaml
require 'yaml'
to_hash.to_yaml
end
private
# @group Private Helpers
#-------------------------------------------------------------------------#
# Loads the specification for the given Pod gracefully.
#
# @param [String] name
# the name of the Pod.
#
# @return [Specification] The specification for the last version of the
# Pod.
# @return [Nil] If the spec could not be loaded.
#
def load_spec_gracefully(name)
versions = versions(name)
version = versions.sort.last if versions
specification(name, version) if version
rescue Informative
Pod::CoreUI.warn "Skipping `#{name}` because the podspec " \
'contains errors.'
nil
end
def refresh_metadata
@metadata = Metadata.from_file(metadata_path)
end
def git_commit_hash
repo_git(%w(rev-parse HEAD))
end
def update_git_repo(show_output = false)
repo_git(['checkout', git_tracking_branch])
output = repo_git(%w(pull --ff-only), :include_error => true)
CoreUI.puts output if show_output
end
def git_tracking_branch
path = repo.join('.git', 'cocoapods_branch')
path.file? ? path.read.strip : DEFAULT_SPECS_BRANCH
end
def diff_until_commit_hash(commit_hash)
repo_git(%W(diff --name-only #{commit_hash}..HEAD)).split("\n")
end
def repo_git(args, include_error: false)
command = "env -u GIT_CONFIG git -C \"#{repo}\" " << args.join(' ')
command << ' 2>&1' if include_error
(`#{command}` || '').strip
end
def unchanged_github_repo?
return unless url =~ /github.com/
!GitHub.modified_since_commit(url, git_commit_hash)
end
#-------------------------------------------------------------------------#
end
end