refinement 0.6.0 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +16 -0
- data/VERSION +1 -1
- data/exe/refine +1 -1
- data/lib/cocoapods_plugin.rb +2 -2
- data/lib/refinement.rb +6 -27
- data/lib/sq/refinement/analyzer.rb +396 -0
- data/lib/sq/refinement/annotated_target.rb +82 -0
- data/lib/sq/refinement/changeset/file_modification.rb +144 -0
- data/lib/sq/refinement/changeset.rb +225 -0
- data/lib/sq/refinement/cli.rb +121 -0
- data/lib/sq/refinement/cocoapods_post_install_writer.rb +129 -0
- data/lib/sq/refinement/setup.rb +22 -0
- data/lib/sq/refinement/used_path.rb +156 -0
- data/lib/sq/refinement/version.rb +8 -0
- metadata +15 -27
- data/lib/refinement/analyzer.rb +0 -395
- data/lib/refinement/annotated_target.rb +0 -78
- data/lib/refinement/changeset/file_modification.rb +0 -138
- data/lib/refinement/changeset.rb +0 -223
- data/lib/refinement/cli.rb +0 -119
- data/lib/refinement/cocoapods_post_install_writer.rb +0 -126
- data/lib/refinement/used_path.rb +0 -154
- data/lib/refinement/version.rb +0 -6
@@ -1,78 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Refinement
|
4
|
-
# A target, annotated with any changes
|
5
|
-
class AnnotatedTarget
|
6
|
-
# @return [Xcodeproj::Project::AbstactTarget] the target in an Xcode project
|
7
|
-
attr_reader :xcode_target
|
8
|
-
|
9
|
-
# @return [String,Nil] the reason why the target has changed, or `nil` if it has not changed
|
10
|
-
attr_reader :direct_change_reason
|
11
|
-
private :direct_change_reason
|
12
|
-
|
13
|
-
def initialize(target:, change_reason:, dependencies: [])
|
14
|
-
@xcode_target = target
|
15
|
-
@direct_change_reason = change_reason
|
16
|
-
@dependencies = dependencies
|
17
|
-
dependencies.each do |dependency|
|
18
|
-
dependency.depended_upon_by << self
|
19
|
-
end
|
20
|
-
@depended_upon_by = []
|
21
|
-
end
|
22
|
-
|
23
|
-
# @visibility private
|
24
|
-
def to_s
|
25
|
-
xcode_target.to_s
|
26
|
-
end
|
27
|
-
|
28
|
-
CHANGE_LEVELS = %i[
|
29
|
-
itself
|
30
|
-
at_most_n_away
|
31
|
-
full_transitive
|
32
|
-
].freeze
|
33
|
-
private_constant :CHANGE_LEVELS
|
34
|
-
|
35
|
-
# @return [Boolean] whether the target has changed, at the given change level
|
36
|
-
# @param level [Symbol,(:at_most_n_away,Integer)] change level, e.g. :itself, :at_most_n_away, :full_transitive
|
37
|
-
def change_reason(level:)
|
38
|
-
@change_reason ||= {}
|
39
|
-
# need to use this form for memoization, as opposed to ||=,
|
40
|
-
# since this will (often) be nil and it makes a significant performance difference
|
41
|
-
return @change_reason[level] if @change_reason.key?(level)
|
42
|
-
|
43
|
-
@change_reason[level] =
|
44
|
-
case level
|
45
|
-
when :itself
|
46
|
-
direct_change_reason
|
47
|
-
when :full_transitive
|
48
|
-
direct_change_reason || Refinement.map_find(dependencies) do |dependency|
|
49
|
-
next unless (dependency_change_reason = dependency.change_reason(level: level))
|
50
|
-
|
51
|
-
"dependency #{dependency} changed because #{dependency_change_reason}"
|
52
|
-
end
|
53
|
-
when proc { |symbol, int| (symbol == :at_most_n_away) && int.is_a?(Integer) }
|
54
|
-
distance_from_target = level.last
|
55
|
-
raise ArgumentError, "level must be positive, not #{distance_from_target}" if distance_from_target.negative?
|
56
|
-
|
57
|
-
change_reason = direct_change_reason
|
58
|
-
if distance_from_target.positive?
|
59
|
-
change_reason ||= Refinement.map_find(dependencies) do |dependency|
|
60
|
-
unless (dependency_change_reason = dependency.change_reason(level: [:at_most_n_away, level.last.pred]))
|
61
|
-
next
|
62
|
-
end
|
63
|
-
|
64
|
-
"dependency #{dependency} changed because #{dependency_change_reason}"
|
65
|
-
end
|
66
|
-
end
|
67
|
-
change_reason
|
68
|
-
else
|
69
|
-
raise Error, "No known change level #{level.inspect}, only #{CHANGE_LEVELS.inspect} are known"
|
70
|
-
end
|
71
|
-
end
|
72
|
-
|
73
|
-
# @return [Array<AnnotatedTarget>] the list of annotated targets this target depends upon
|
74
|
-
attr_reader :dependencies
|
75
|
-
# @return [Array<AnnotatedTarget>] the list of annotated targets that depend upon this target
|
76
|
-
attr_reader :depended_upon_by
|
77
|
-
end
|
78
|
-
end
|
@@ -1,138 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Refinement
|
4
|
-
class Changeset
|
5
|
-
# Represents a modification to a single file or directory on disk
|
6
|
-
class FileModification
|
7
|
-
# @return [Symbol] the change type for directories
|
8
|
-
DIRECTORY_CHANGE_TYPE = :'had contents change'
|
9
|
-
|
10
|
-
# @return [Pathname] the path to the modified file
|
11
|
-
attr_reader :path
|
12
|
-
|
13
|
-
# @return [Pathname, Nil] the prior path to the modified file, or `nil` if it was not renamed or copied
|
14
|
-
attr_reader :prior_path
|
15
|
-
|
16
|
-
# @return [#to_s] the type of change that happened to this file
|
17
|
-
attr_reader :type
|
18
|
-
|
19
|
-
def initialize(path:, type:,
|
20
|
-
prior_path: nil,
|
21
|
-
contents_reader: -> { nil },
|
22
|
-
prior_contents_reader: -> { nil })
|
23
|
-
@path = path
|
24
|
-
@type = type
|
25
|
-
@prior_path = prior_path
|
26
|
-
@contents_reader = contents_reader
|
27
|
-
@prior_contents_reader = prior_contents_reader
|
28
|
-
end
|
29
|
-
|
30
|
-
# @visibility private
|
31
|
-
def to_s
|
32
|
-
case type
|
33
|
-
when DIRECTORY_CHANGE_TYPE
|
34
|
-
"contents of dir `#{path}` changed"
|
35
|
-
else
|
36
|
-
message = "file `#{path}` #{type}"
|
37
|
-
message += " (from #{prior_path})" if prior_path
|
38
|
-
message
|
39
|
-
end
|
40
|
-
end
|
41
|
-
|
42
|
-
# @visibility private
|
43
|
-
def inspect
|
44
|
-
"#<#{self.class} path=#{path.inspect} type=#{type.inspect} prior_path=#{prior_path.inspect}" \
|
45
|
-
" contents=#{contents.inspect} prior_contents=#{prior_contents.inspect}>"
|
46
|
-
end
|
47
|
-
|
48
|
-
# @visibility private
|
49
|
-
def hash
|
50
|
-
path.hash ^ type.hash
|
51
|
-
end
|
52
|
-
|
53
|
-
# @visibility private
|
54
|
-
def ==(other)
|
55
|
-
return unless other.is_a?(FileModification)
|
56
|
-
|
57
|
-
(path == other.path) && (type == other.type) && prior_path == other.prior_path
|
58
|
-
end
|
59
|
-
|
60
|
-
# @visibility private
|
61
|
-
def eql?(other)
|
62
|
-
return unless other.is_a?(FileModification)
|
63
|
-
|
64
|
-
path.eql?(other.path) && type.eql?(other.type) && prior_path.eql?(other.prior_path)
|
65
|
-
end
|
66
|
-
|
67
|
-
# @return [String,Nil] a YAML string representing the diff of the file
|
68
|
-
# from the prior revision to the current revision at the given keypath
|
69
|
-
# in the YAML, or `nil` if there is no diff
|
70
|
-
# @param keypath [Array] a list of indices passed to `dig`.
|
71
|
-
# An empty array is equivalent to the entire YAML document
|
72
|
-
def yaml_diff(keypath)
|
73
|
-
require 'yaml'
|
74
|
-
|
75
|
-
@cached_yaml ||= {}
|
76
|
-
|
77
|
-
dig_yaml = lambda do |yaml, path|
|
78
|
-
return yaml if DOES_NOT_EXIST == yaml
|
79
|
-
|
80
|
-
object = @cached_yaml[path] ||= YAML.safe_load(yaml, [Symbol])
|
81
|
-
if keypath.empty?
|
82
|
-
object
|
83
|
-
elsif object.respond_to?(:dig)
|
84
|
-
object.dig(*keypath)
|
85
|
-
else # backwards compatibility
|
86
|
-
keypath.reduce(object) do |acc, elem|
|
87
|
-
acc[elem]
|
88
|
-
end
|
89
|
-
end
|
90
|
-
end
|
91
|
-
|
92
|
-
prior = dig_yaml[prior_contents, :prior]
|
93
|
-
current = dig_yaml[contents, :current]
|
94
|
-
|
95
|
-
require 'xcodeproj/differ'
|
96
|
-
|
97
|
-
return unless (diff = Xcodeproj::Differ.diff(
|
98
|
-
prior,
|
99
|
-
current,
|
100
|
-
key_1: 'prior_revision',
|
101
|
-
key_2: 'current_revision'
|
102
|
-
))
|
103
|
-
|
104
|
-
diff.to_yaml.prepend("#{path} changed at keypath #{keypath.inspect}\n")
|
105
|
-
end
|
106
|
-
|
107
|
-
DOES_NOT_EXIST = Object.new.tap do |o|
|
108
|
-
class << o
|
109
|
-
def to_s
|
110
|
-
'DOES NOT EXISTS'
|
111
|
-
end
|
112
|
-
alias_method :inspect, :to_s
|
113
|
-
end
|
114
|
-
end.freeze
|
115
|
-
private_constant :DOES_NOT_EXIST
|
116
|
-
|
117
|
-
# @return [String] the current contents of the file
|
118
|
-
def contents
|
119
|
-
@contents ||=
|
120
|
-
begin
|
121
|
-
@contents_reader[].tap { @contents_reader = nil } || DOES_NOT_EXIST
|
122
|
-
rescue StandardError
|
123
|
-
DOES_NOT_EXIST
|
124
|
-
end
|
125
|
-
end
|
126
|
-
|
127
|
-
# @return [String] the prior contents of the file
|
128
|
-
def prior_contents
|
129
|
-
@prior_contents ||=
|
130
|
-
begin
|
131
|
-
@prior_contents_reader[].tap { @prior_contents_reader = nil } || DOES_NOT_EXIST
|
132
|
-
rescue StandardError
|
133
|
-
DOES_NOT_EXIST
|
134
|
-
end
|
135
|
-
end
|
136
|
-
end
|
137
|
-
end
|
138
|
-
end
|
data/lib/refinement/changeset.rb
DELETED
@@ -1,223 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'cocoapods/executable'
|
4
|
-
require 'set'
|
5
|
-
|
6
|
-
module Refinement
|
7
|
-
# Represents a set of changes in a repository between a prior revision and the current state
|
8
|
-
class Changeset
|
9
|
-
# An error that happens when computing a git diff
|
10
|
-
class GitError < Error; end
|
11
|
-
require 'refinement/changeset/file_modification'
|
12
|
-
|
13
|
-
# @return [Pathname] the path to the repository
|
14
|
-
attr_reader :repository
|
15
|
-
# @return [Array<FileModification>] the modifications in the changeset
|
16
|
-
attr_reader :modifications
|
17
|
-
# @return [Hash<Pathname,FileModification>] modifications keyed by relative path
|
18
|
-
attr_reader :modified_paths
|
19
|
-
# @return [Hash<Pathname,FileModification>] modifications keyed by relative path
|
20
|
-
attr_reader :modified_absolute_paths
|
21
|
-
# @return [String] a desciption of the changeset
|
22
|
-
attr_reader :description
|
23
|
-
|
24
|
-
private :modifications, :modified_paths, :modified_absolute_paths
|
25
|
-
|
26
|
-
def initialize(repository:, modifications:, description: nil)
|
27
|
-
@repository = repository
|
28
|
-
@modifications = self.class.add_directories(modifications).uniq.freeze
|
29
|
-
@description = description
|
30
|
-
|
31
|
-
@modified_paths = {}
|
32
|
-
@modifications
|
33
|
-
.each { |mod| @modified_paths[mod.path] = mod }
|
34
|
-
.each { |mod| @modified_paths[mod.prior_path] ||= mod if mod.prior_path }
|
35
|
-
@modified_paths.freeze
|
36
|
-
|
37
|
-
@modified_absolute_paths = {}
|
38
|
-
@modified_paths
|
39
|
-
.each { |path, mod| @modified_absolute_paths[path.expand_path(repository).freeze] = mod }
|
40
|
-
@modified_absolute_paths.freeze
|
41
|
-
end
|
42
|
-
|
43
|
-
# @visibility private
|
44
|
-
# @return [Array<FileModification>] file modifications that include modifications for each
|
45
|
-
# directory that has had a child modified
|
46
|
-
# @param modifications [Array<FileModification>] The modifications to add directory modifications to
|
47
|
-
def self.add_directories(modifications)
|
48
|
-
dirs = Set.new
|
49
|
-
add = lambda { |path|
|
50
|
-
break unless dirs.add?(path)
|
51
|
-
|
52
|
-
add[path.dirname]
|
53
|
-
}
|
54
|
-
modifications.each do |mod|
|
55
|
-
add[mod.path.dirname]
|
56
|
-
add[mod.prior_path.dirname] if mod.prior_path
|
57
|
-
end
|
58
|
-
modifications +
|
59
|
-
dirs.map { |d| FileModification.new(path: Pathname("#{d}/").freeze, type: FileModification::DIRECTORY_CHANGE_TYPE) }
|
60
|
-
end
|
61
|
-
|
62
|
-
# @return [FileModification,Nil] the changeset for the given absolute path,
|
63
|
-
# or `nil` if the given path is un-modified
|
64
|
-
# @param absolute_path [Pathname]
|
65
|
-
def find_modification_for_path(absolute_path:)
|
66
|
-
modified_absolute_paths[absolute_path]
|
67
|
-
end
|
68
|
-
|
69
|
-
# @return [Array<String>] An array of patterns converted from a
|
70
|
-
# {Dir.glob} pattern to patterns that {File.fnmatch} can handle.
|
71
|
-
# This is used by the {#relative_glob} method to emulate
|
72
|
-
# {Dir.glob}.
|
73
|
-
#
|
74
|
-
# The expansion provides support for:
|
75
|
-
#
|
76
|
-
# - Literals
|
77
|
-
#
|
78
|
-
# dir_glob_equivalent_patterns('{file1,file2}.{h,m}')
|
79
|
-
# => ["file1.h", "file1.m", "file2.h", "file2.m"]
|
80
|
-
#
|
81
|
-
# - Matching the direct children of a directory with `**`
|
82
|
-
#
|
83
|
-
# dir_glob_equivalent_patterns('Classes/**/file.m')
|
84
|
-
# => ["Classes/**/file.m", "Classes/file.m"]
|
85
|
-
#
|
86
|
-
# @param [String] pattern A {Dir#glob} like pattern.
|
87
|
-
#
|
88
|
-
def dir_glob_equivalent_patterns(pattern)
|
89
|
-
pattern = pattern.gsub('/**/', '{/**/,/}')
|
90
|
-
values_by_set = {}
|
91
|
-
pattern.scan(/\{[^}]*\}/) do |set|
|
92
|
-
values = set.gsub(/[{}]/, '').split(',', -1)
|
93
|
-
values_by_set[set] = values
|
94
|
-
end
|
95
|
-
|
96
|
-
if values_by_set.empty?
|
97
|
-
[pattern]
|
98
|
-
else
|
99
|
-
patterns = [pattern]
|
100
|
-
values_by_set.each do |set, values|
|
101
|
-
patterns = patterns.flat_map do |old_pattern|
|
102
|
-
values.map do |value|
|
103
|
-
old_pattern.gsub(set, value)
|
104
|
-
end
|
105
|
-
end
|
106
|
-
end
|
107
|
-
patterns
|
108
|
-
end
|
109
|
-
end
|
110
|
-
private :dir_glob_equivalent_patterns
|
111
|
-
|
112
|
-
# @return [FileModification,Nil] the modification for the given absolute glob,
|
113
|
-
# or `nil` if no files matching the glob were modified
|
114
|
-
# @note Will only return a single (arbitrary) matching modification, even if there are
|
115
|
-
# multiple modifications that match the glob
|
116
|
-
# @param absolute_glob [String] a glob pattern for absolute paths, suitable for an invocation of `Dir.glob`
|
117
|
-
def find_modification_for_glob(absolute_glob:)
|
118
|
-
absolute_globs = dir_glob_equivalent_patterns(absolute_glob)
|
119
|
-
_path, modification = modified_absolute_paths.find do |absolute_path, _modification|
|
120
|
-
absolute_globs.any? do |glob|
|
121
|
-
File.fnmatch?(glob, absolute_path, File::FNM_CASEFOLD | File::FNM_PATHNAME)
|
122
|
-
end
|
123
|
-
end
|
124
|
-
modification
|
125
|
-
end
|
126
|
-
|
127
|
-
# @return [FileModification,Nil] a modification and yaml diff for the keypath at the given absolute path,
|
128
|
-
# or `nil` if the value at the given keypath is un-modified
|
129
|
-
# @param absolute_path [Pathname]
|
130
|
-
# @param keypath [Array]
|
131
|
-
def find_modification_for_yaml_keypath(absolute_path:, keypath:)
|
132
|
-
return unless (file_modification = find_modification_for_path(absolute_path: absolute_path))
|
133
|
-
|
134
|
-
diff = file_modification.yaml_diff(keypath)
|
135
|
-
return unless diff
|
136
|
-
|
137
|
-
[file_modification, diff]
|
138
|
-
end
|
139
|
-
|
140
|
-
# @return [Changeset] the changes in the given git repository between the given revision and HEAD
|
141
|
-
# @param repository [Pathname]
|
142
|
-
# @param base_revision [String]
|
143
|
-
def self.from_git(repository:, base_revision:)
|
144
|
-
raise ArgumentError, "must be given a Pathname for repository, got #{repository.inspect}" unless repository.is_a?(Pathname)
|
145
|
-
raise ArgumentError, "must be given a String for base_revision, got #{base_revision.inspect}" unless base_revision.is_a?(String)
|
146
|
-
|
147
|
-
merge_base = git!('merge-base', base_revision, 'HEAD', chdir: repository).strip
|
148
|
-
diff = git!('diff', '--raw', '-z', merge_base, chdir: repository)
|
149
|
-
modifications = parse_raw_diff(diff, repository: repository, base_revision: merge_base).freeze
|
150
|
-
|
151
|
-
new(repository: repository, modifications: modifications, description: "since #{base_revision}")
|
152
|
-
end
|
153
|
-
|
154
|
-
CHANGE_TYPES = {
|
155
|
-
'was added': 'A',
|
156
|
-
'was copied': 'C',
|
157
|
-
'was deleted': 'D',
|
158
|
-
'was modified': 'M',
|
159
|
-
'was renamed': 'R',
|
160
|
-
'changed type': 'T',
|
161
|
-
'is unmerged': 'U',
|
162
|
-
'changed in an unknown way': 'X'
|
163
|
-
}.freeze
|
164
|
-
private_constant :CHANGE_TYPES
|
165
|
-
|
166
|
-
CHANGE_CHARACTERS = CHANGE_TYPES.invert.freeze
|
167
|
-
private_constant :CHANGE_CHARACTERS
|
168
|
-
|
169
|
-
# Parses the raw diff into FileModification objects
|
170
|
-
# @return [Array<FileModification>]
|
171
|
-
# @param diff [String] a diff generated by `git diff --raw -z`
|
172
|
-
# @param repository [Pathname] the path to the repository
|
173
|
-
# @param base_revision [String] the base revision the diff was constructed agains
|
174
|
-
def self.parse_raw_diff(diff, repository:, base_revision:)
|
175
|
-
# since we're null separating the chunks (to avoid dealing with path escaping) we have to reconstruct
|
176
|
-
# the chunks into individual diff entries. entries always start with a colon so we can use that to signal if
|
177
|
-
# we're on a new entry
|
178
|
-
parsed_lines = diff.split("\0").each_with_object([]) do |chunk, lines|
|
179
|
-
lines << [] if chunk.start_with?(':')
|
180
|
-
lines.last << chunk
|
181
|
-
end
|
182
|
-
|
183
|
-
parsed_lines.map do |split_line|
|
184
|
-
# change chunk (letter + optional similarity percentage) will always be the last part of first line chunk
|
185
|
-
change_chunk = split_line[0].split(/\s/).last
|
186
|
-
|
187
|
-
new_path = Pathname(split_line[2]).freeze if split_line[2]
|
188
|
-
old_path = Pathname(split_line[1]).freeze
|
189
|
-
prior_path = old_path if new_path
|
190
|
-
# new path if one exists, else existing path. new path only exists for rename and copy
|
191
|
-
changed_path = new_path || old_path
|
192
|
-
|
193
|
-
change_character = change_chunk[0]
|
194
|
-
# returns 0 when a similarity percentage isn't specified by git.
|
195
|
-
_similarity = change_chunk[1..3].to_i
|
196
|
-
|
197
|
-
FileModification.new(
|
198
|
-
path: changed_path,
|
199
|
-
type: CHANGE_CHARACTERS[change_character],
|
200
|
-
prior_path: prior_path,
|
201
|
-
contents_reader: -> { repository.join(changed_path).read },
|
202
|
-
prior_contents_reader: lambda {
|
203
|
-
git!('show', "#{base_revision}:#{prior_path || changed_path}", chdir: repository)
|
204
|
-
}
|
205
|
-
)
|
206
|
-
end
|
207
|
-
end
|
208
|
-
|
209
|
-
# @return [String] the STDOUT of the git command
|
210
|
-
# @raise [GitError] when running the git command fails
|
211
|
-
# @param command [String] the base git command to run
|
212
|
-
# @param args [String...] arguments to the git command
|
213
|
-
# @param chdir [String,Pathname] the directory to run the git command in
|
214
|
-
def self.git!(command, *args, chdir:)
|
215
|
-
require 'open3'
|
216
|
-
out, err, status = Open3.capture3('git', command, *args, chdir: chdir.to_s)
|
217
|
-
raise GitError, "Running git #{command} failed (#{status.to_s.gsub(/pid \d+\s*/, '')}):\n\n#{err}" unless status.success?
|
218
|
-
|
219
|
-
out
|
220
|
-
end
|
221
|
-
private_class_method :git!
|
222
|
-
end
|
223
|
-
end
|
data/lib/refinement/cli.rb
DELETED
@@ -1,119 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require 'claide'
|
4
|
-
|
5
|
-
module Refinement
|
6
|
-
# @visibility private
|
7
|
-
class CLI < CLAide::Command
|
8
|
-
self.abstract_command = true
|
9
|
-
self.command = 'refine'
|
10
|
-
self.version = VERSION
|
11
|
-
|
12
|
-
self.summary = 'Generates a list of Xcode targets to build & test as a result of a diff'
|
13
|
-
|
14
|
-
def self.options
|
15
|
-
super + [
|
16
|
-
['--repository=REPOSITORY', 'Path to repository'],
|
17
|
-
['--workspace=WORKSPACE_PATH', 'Path to project or workspace'],
|
18
|
-
['--scheme=SCHEME_PATH', 'Path to scheme to be filtered'],
|
19
|
-
['--augmenting-paths-yaml-files=PATH1,PATH2...', 'Paths to augmenting yaml files, relative to the repository path'],
|
20
|
-
['--[no-]print-changes', 'Print the change reason for changed targets'],
|
21
|
-
['--[no-]print-scheme-changes', 'Print the change reason for targets in the given scheme'],
|
22
|
-
['--change-level=LEVEL', 'Change level at which a target must have changed in order to be considered changed. ' \
|
23
|
-
'One of `full-transitive`, `itself`, or an integer'],
|
24
|
-
['--filter-scheme-for-build-action=BUILD_ACTION', 'The xcodebuild action the scheme (if given) is filtered for. ' \
|
25
|
-
'One of `building` or `testing`.']
|
26
|
-
]
|
27
|
-
end
|
28
|
-
|
29
|
-
def initialize(argv)
|
30
|
-
@repository = argv.option('repository', '.')
|
31
|
-
@workspace = argv.option('workspace')
|
32
|
-
@scheme = argv.option('scheme')
|
33
|
-
@augmenting_paths_yaml_files = argv.option('augmenting-paths-yaml-files', '')
|
34
|
-
@print_changes = argv.flag?('print-changes', false)
|
35
|
-
@print_scheme_changes = argv.flag?('print-scheme-changes', false)
|
36
|
-
@change_level = argv.option('change-level', 'full-transitive')
|
37
|
-
@filter_scheme_for_build_action = argv.option('filter-scheme-for-build-action', 'testing').to_sym
|
38
|
-
|
39
|
-
super
|
40
|
-
end
|
41
|
-
|
42
|
-
def run
|
43
|
-
changeset = compute_changeset
|
44
|
-
|
45
|
-
analyzer = Refinement::Analyzer.new(changeset: changeset,
|
46
|
-
workspace_path: @workspace,
|
47
|
-
augmenting_paths_yaml_files: @augmenting_paths_yaml_files)
|
48
|
-
analyzer.annotate_targets!
|
49
|
-
|
50
|
-
puts analyzer.format_changes if @print_changes
|
51
|
-
|
52
|
-
return unless @scheme
|
53
|
-
|
54
|
-
analyzer.filtered_scheme(scheme_path: @scheme, log_changes: @print_scheme_changes, filter_scheme_for_build_action: @filter_scheme_for_build_action)
|
55
|
-
.save_as(@scheme.gsub(%r{\.(xcodeproj|xcworkspace)/.+}, '.\1'), File.basename(@scheme, '.xcscheme'), true)
|
56
|
-
end
|
57
|
-
|
58
|
-
def validate!
|
59
|
-
super
|
60
|
-
|
61
|
-
File.directory?(@repository) || help!("Unable to find a repository at #{@repository.inspect}")
|
62
|
-
|
63
|
-
@workspace || help!('Must specify a project or workspace path')
|
64
|
-
File.directory?(@workspace) || help!("Unable to find a project or workspace at #{@workspace.inspect}")
|
65
|
-
|
66
|
-
@augmenting_paths_yaml_files = @augmenting_paths_yaml_files.split(',')
|
67
|
-
@augmenting_paths_yaml_files.each do |yaml_path|
|
68
|
-
yaml_path = File.join(@repository, yaml_path)
|
69
|
-
File.file?(yaml_path) || help!("Unable to find a YAML file at #{yaml_path.inspect}")
|
70
|
-
|
71
|
-
require 'yaml'
|
72
|
-
begin
|
73
|
-
YAML.safe_load(File.read(yaml_path))
|
74
|
-
rescue StandardError => e
|
75
|
-
help! "Failed to load YAML file at #{yaml_path.inspect} (#{e})"
|
76
|
-
end
|
77
|
-
end
|
78
|
-
|
79
|
-
File.file?(@scheme) || help!("Unabled to find a scheme at #{@scheme.inspect}") if @scheme
|
80
|
-
|
81
|
-
@change_level =
|
82
|
-
case @change_level
|
83
|
-
when 'full-transitive' then :full_transitive
|
84
|
-
when 'itself' then :itself
|
85
|
-
when /\A\d+\z/ then [:at_most_n_away, @change_level.to_i]
|
86
|
-
else help! "Unknown change level #{@change_level.inspect}"
|
87
|
-
end
|
88
|
-
end
|
89
|
-
|
90
|
-
# @visibility private
|
91
|
-
class Git < CLI
|
92
|
-
self.summary = 'Generates a list of Xcode targets to build & test as a result of a git diff'
|
93
|
-
|
94
|
-
def self.options
|
95
|
-
super + [
|
96
|
-
['--base-revision=SHA', 'Base revision to compute the git diff against']
|
97
|
-
]
|
98
|
-
end
|
99
|
-
|
100
|
-
def initialize(argv)
|
101
|
-
@base_revision = argv.option('base-revision')
|
102
|
-
|
103
|
-
super
|
104
|
-
end
|
105
|
-
|
106
|
-
def validate!
|
107
|
-
super
|
108
|
-
|
109
|
-
@base_revision || help!('Must specify a base revision')
|
110
|
-
end
|
111
|
-
|
112
|
-
private
|
113
|
-
|
114
|
-
def compute_changeset
|
115
|
-
Refinement::Changeset.from_git(repository: Pathname(@repository), base_revision: @base_revision)
|
116
|
-
end
|
117
|
-
end
|
118
|
-
end
|
119
|
-
end
|
@@ -1,126 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module Refinement
|
4
|
-
# Called after CocoaPods installation to write an augmenting file that
|
5
|
-
# takes into account changes to Pod configuration,
|
6
|
-
# as well as the globs used by podspecs to search for files
|
7
|
-
class CocoaPodsPostInstallWriter
|
8
|
-
attr_reader :aggregate_targets, :pod_targets, :config, :repo, :options
|
9
|
-
private :aggregate_targets, :pod_targets, :config, :repo, :options
|
10
|
-
|
11
|
-
# Initializes a post-install writer with CocoaPods target objects.
|
12
|
-
# @return [CocoaPodsPostInstallWriter] a new instance of CocoaPodsPostInstallWriter
|
13
|
-
# @param aggregate_targets [Array<Pod::AggregateTarget>]
|
14
|
-
# @param pod_targets [Array<Pod::PodTarget>]
|
15
|
-
# @param config [Pod::Config]
|
16
|
-
# @param options [Hash]
|
17
|
-
def initialize(aggregate_targets, pod_targets, config, options)
|
18
|
-
@aggregate_targets = aggregate_targets
|
19
|
-
@pod_targets = pod_targets
|
20
|
-
@config = config
|
21
|
-
@repo = config.installation_root
|
22
|
-
@options = options || {}
|
23
|
-
end
|
24
|
-
|
25
|
-
# Writes the refinement augmenting file to the configured path
|
26
|
-
# @return [Void]
|
27
|
-
def write!
|
28
|
-
write_file options.fetch('output_path', config.sandbox.root.join('pods_refinement.json')), paths_by_target_name
|
29
|
-
end
|
30
|
-
|
31
|
-
private
|
32
|
-
|
33
|
-
def write_file(path, hash)
|
34
|
-
require 'json'
|
35
|
-
File.open(path, 'w') do |f|
|
36
|
-
f << JSON.generate(hash) << "\n"
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
def paths_by_target_name
|
41
|
-
targets = {}
|
42
|
-
aggregate_targets.each do |aggregate_target|
|
43
|
-
targets[aggregate_target.label] = paths_for_aggregate_target(aggregate_target)
|
44
|
-
end
|
45
|
-
pod_targets.each do |pod_target|
|
46
|
-
targets.merge! paths_for_pod_targets(pod_target)
|
47
|
-
end
|
48
|
-
targets
|
49
|
-
end
|
50
|
-
|
51
|
-
def paths_for_aggregate_target(aggregate_target)
|
52
|
-
paths = []
|
53
|
-
if (podfile_path = aggregate_target.podfile.defined_in_file)
|
54
|
-
paths << { path: podfile_path.relative_path_from(repo), inclusion_reason: 'Podfile' }
|
55
|
-
end
|
56
|
-
if (user_project_path = aggregate_target.user_project_path)
|
57
|
-
paths << { path: user_project_path.relative_path_from(repo), inclusion_reason: 'user project' }
|
58
|
-
end
|
59
|
-
paths
|
60
|
-
end
|
61
|
-
|
62
|
-
def library_specification?(spec)
|
63
|
-
# Backwards compatibility
|
64
|
-
if spec.respond_to?(:library_specification?, false)
|
65
|
-
spec.library_specification?
|
66
|
-
else
|
67
|
-
!spec.test_specification?
|
68
|
-
end
|
69
|
-
end
|
70
|
-
|
71
|
-
def specification_paths_from_pod_target(pod_target)
|
72
|
-
pod_target
|
73
|
-
.target_definitions
|
74
|
-
.map(&:podfile)
|
75
|
-
.uniq
|
76
|
-
.flat_map do |podfile|
|
77
|
-
podfile
|
78
|
-
.dependencies
|
79
|
-
.select { |d| d.root_name == pod_target.pod_name }
|
80
|
-
.map { |d| (d.external_source || {})[:path] }
|
81
|
-
.compact
|
82
|
-
end.uniq
|
83
|
-
end
|
84
|
-
|
85
|
-
def paths_for_pod_targets(pod_target)
|
86
|
-
file_accessors_by_target_name = pod_target.file_accessors.group_by do |fa|
|
87
|
-
if library_specification?(fa.spec)
|
88
|
-
pod_target.label
|
89
|
-
elsif pod_target.respond_to?(:non_library_spec_label)
|
90
|
-
pod_target.non_library_spec_label(fa.spec)
|
91
|
-
else
|
92
|
-
pod_target.test_target_label(fa.spec)
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
pod_dir = pod_target.sandbox.pod_dir(pod_target.pod_name).relative_path_from(repo)
|
97
|
-
|
98
|
-
spec_paths = specification_paths_from_pod_target(pod_target)
|
99
|
-
|
100
|
-
file_accessors_by_target_name.each_with_object({}) do |(label, file_accessors), h|
|
101
|
-
paths = [
|
102
|
-
{ path: 'Podfile.lock',
|
103
|
-
inclusion_reason: 'CocoaPods lockfile',
|
104
|
-
yaml_keypath: ['SPEC CHECKSUMS', pod_target.pod_name] }
|
105
|
-
]
|
106
|
-
if pod_target.sandbox.predownloaded?(pod_target.pod_name)
|
107
|
-
paths << { path: 'Podfile.lock', inclusion_reason: 'Dependency external source', yaml_keypath: ['EXTERNAL SOURCES', pod_target.pod_name] }
|
108
|
-
paths << { path: 'Podfile.lock', inclusion_reason: 'Pod checkout options', yaml_keypath: ['CHECKOUT OPTIONS', pod_target.pod_name] }
|
109
|
-
end
|
110
|
-
spec_paths.each { |path| paths << { path: path, inclusion_reason: 'podspec' } }
|
111
|
-
|
112
|
-
Pod::Validator::FILE_PATTERNS.each do |pattern|
|
113
|
-
file_accessors.each do |fa|
|
114
|
-
globs = fa.spec_consumer.send(pattern)
|
115
|
-
globs = globs.values.flatten if globs.is_a?(Hash)
|
116
|
-
globs.each do |glob|
|
117
|
-
paths << { glob: pod_dir.join(glob), inclusion_reason: pattern.to_s.tr('_', ' ').chomp('s') }
|
118
|
-
end
|
119
|
-
end
|
120
|
-
end
|
121
|
-
|
122
|
-
h[label] = paths.uniq
|
123
|
-
end
|
124
|
-
end
|
125
|
-
end
|
126
|
-
end
|