kintsugi 0.1.0
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.
- checksums.yaml +7 -0
- data/.github/workflows/ci.yml +25 -0
- data/.gitignore +56 -0
- data/.rspec +1 -0
- data/.rubocop.yml +221 -0
- data/CONTRIBUTING.md +23 -0
- data/Gemfile +4 -0
- data/LICENSE +21 -0
- data/README.md +51 -0
- data/Rakefile +13 -0
- data/bin/kintsugi +44 -0
- data/kintsugi.gemspec +30 -0
- data/lib/kintsugi.rb +119 -0
- data/lib/kintsugi/apply_change_to_project.rb +592 -0
- data/lib/kintsugi/utils.rb +37 -0
- data/lib/kintsugi/xcodeproj_extensions.rb +31 -0
- data/logo/kintsugi.png +0 -0
- data/spec/be_equivalent_to_project.rb +63 -0
- data/spec/kintsugi_apply_change_to_project_spec.rb +885 -0
- data/spec/spec_helper.rb +105 -0
- metadata +151 -0
data/kintsugi.gemspec
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
Gem::Specification.new do |spec|
|
4
|
+
spec.name = "kintsugi"
|
5
|
+
spec.version = "0.1.0"
|
6
|
+
spec.authors = ["Ben Yohay"]
|
7
|
+
spec.email = ["ben@lightricks.com"]
|
8
|
+
spec.required_ruby_version = ">= 2.5.0"
|
9
|
+
spec.description =
|
10
|
+
%q(
|
11
|
+
Kintsugi resolves conflicts in .pbxproj files, with the aim to resolve 99.9% of the conflicts
|
12
|
+
automatically.
|
13
|
+
)
|
14
|
+
spec.summary = %q(pbxproj files git conflicts solver)
|
15
|
+
spec.homepage = "https://github.com/Lightricks/Kintsugi"
|
16
|
+
spec.license = "MIT"
|
17
|
+
|
18
|
+
spec.files = `git ls-files`.split($/)
|
19
|
+
spec.executables = spec.files.grep(%r{^bin/}) { |f| File.basename(f) }
|
20
|
+
spec.test_files = spec.files.grep(%r{^(spec)/})
|
21
|
+
spec.require_paths = ["lib"]
|
22
|
+
|
23
|
+
spec.add_dependency "xcodeproj", "1.19.0"
|
24
|
+
|
25
|
+
spec.add_development_dependency "rake", "~> 13.0"
|
26
|
+
spec.add_development_dependency "rspec", "~> 3.9"
|
27
|
+
spec.add_development_dependency "rubocop", "1.12.0"
|
28
|
+
spec.add_development_dependency "rubocop-rspec", "2.2.0"
|
29
|
+
spec.add_development_dependency "simplecov", "~> 0.21"
|
30
|
+
end
|
data/lib/kintsugi.rb
ADDED
@@ -0,0 +1,119 @@
|
|
1
|
+
# Copyright (c) 2021 Lightricks. All rights reserved.
|
2
|
+
# Created by Ben Yohay.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "tmpdir"
|
6
|
+
require "tempfile"
|
7
|
+
require "xcodeproj"
|
8
|
+
|
9
|
+
require_relative "kintsugi/xcodeproj_extensions"
|
10
|
+
require_relative "kintsugi/apply_change_to_project"
|
11
|
+
|
12
|
+
module Kintsugi
|
13
|
+
class << self
|
14
|
+
# Resolves git conflicts of a pbxproj file specified by `project_file_path`.
|
15
|
+
#
|
16
|
+
# @param [String] project_file_path
|
17
|
+
# Project to which to apply the changes.
|
18
|
+
#
|
19
|
+
# @param [String] output_changes_path
|
20
|
+
# Path to where the changes to apply to the project are written in JSON format.
|
21
|
+
#
|
22
|
+
# @raise [ArgumentError]
|
23
|
+
# If the file extension is not `pbxproj` or the file doesn't exist
|
24
|
+
#
|
25
|
+
# @raise [RuntimeError]
|
26
|
+
# If no rebase, cherry-pick, or merge is in progress, or the project file couldn't be
|
27
|
+
# opened, or there was an error applying the change to the project.
|
28
|
+
#
|
29
|
+
# @return [void]
|
30
|
+
def resolve_conflicts(project_file_path, changes_output_path)
|
31
|
+
validate_project(project_file_path)
|
32
|
+
|
33
|
+
project_in_temp_directory =
|
34
|
+
open_project_of_current_commit_in_temporary_directory(project_file_path)
|
35
|
+
|
36
|
+
change = change_of_conflicting_commit_with_parent(project_file_path)
|
37
|
+
|
38
|
+
if changes_output_path
|
39
|
+
File.write(changes_output_path, JSON.pretty_generate(change))
|
40
|
+
end
|
41
|
+
|
42
|
+
apply_change_to_project(project_in_temp_directory, change)
|
43
|
+
|
44
|
+
project_in_temp_directory.save
|
45
|
+
|
46
|
+
Dir.chdir(File.dirname(project_file_path)) do
|
47
|
+
`git reset #{project_file_path}`
|
48
|
+
end
|
49
|
+
FileUtils.cp(File.join(project_in_temp_directory.path, "project.pbxproj"), project_file_path)
|
50
|
+
|
51
|
+
# Some of the metadata in a `pbxproj` file include a part of the name of the directory it's
|
52
|
+
# inside. The modified project is stored in a temporary directory and then copied to the
|
53
|
+
# original path, therefore its metadata is incorrect. To fix this, the project at the original
|
54
|
+
# path is opened and saved.
|
55
|
+
Xcodeproj::Project.open(File.dirname(project_file_path)).save
|
56
|
+
end
|
57
|
+
|
58
|
+
private
|
59
|
+
|
60
|
+
def validate_project(project_file_path)
|
61
|
+
if File.extname(project_file_path) != ".pbxproj"
|
62
|
+
raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
|
63
|
+
end
|
64
|
+
|
65
|
+
unless File.exist?(project_file_path)
|
66
|
+
raise ArgumentError, "File '#{project_file_path}' doesn't exist"
|
67
|
+
end
|
68
|
+
|
69
|
+
Dir.chdir(File.dirname(project_file_path)) do
|
70
|
+
unless file_has_base_ours_and_theirs_versions?(project_file_path)
|
71
|
+
raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, or a 3-way " \
|
72
|
+
"merge is not possible."
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
def open_project_of_current_commit_in_temporary_directory(project_file_path)
|
78
|
+
temp_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
|
79
|
+
Dir.chdir(File.dirname(project_file_path)) do
|
80
|
+
`git show HEAD:./project.pbxproj > #{temp_project_file_path}`
|
81
|
+
end
|
82
|
+
Xcodeproj::Project.open(File.dirname(temp_project_file_path))
|
83
|
+
end
|
84
|
+
|
85
|
+
def file_has_base_ours_and_theirs_versions?(file_path)
|
86
|
+
Dir.chdir(`git rev-parse --show-toplevel`.strip) do
|
87
|
+
file_has_version_in_stage_numbers?(file_path, [1, 2, 3])
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def file_has_version_in_stage_numbers?(file_path, stage_numbers)
|
92
|
+
file_absolute_path = File.absolute_path(file_path)
|
93
|
+
actual_stage_numbers =
|
94
|
+
`git ls-files -u -- #{file_absolute_path}`.split("\n").map do |git_file_status|
|
95
|
+
git_file_status.split[2]
|
96
|
+
end
|
97
|
+
(stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
|
98
|
+
end
|
99
|
+
|
100
|
+
def change_of_conflicting_commit_with_parent(project_file_path)
|
101
|
+
Dir.chdir(File.dirname(project_file_path)) do
|
102
|
+
conflicting_commit_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
|
103
|
+
`git show :3:./project.pbxproj > #{conflicting_commit_project_file_path}`
|
104
|
+
|
105
|
+
conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
|
106
|
+
`git show :1:./project.pbxproj > #{conflicting_commit_parent_project_file_path}`
|
107
|
+
|
108
|
+
conflicting_commit_project = Xcodeproj::Project.open(
|
109
|
+
File.dirname(conflicting_commit_project_file_path)
|
110
|
+
)
|
111
|
+
conflicting_commit_parent_project =
|
112
|
+
Xcodeproj::Project.open(File.dirname(conflicting_commit_parent_project_file_path))
|
113
|
+
|
114
|
+
Xcodeproj::Differ.project_diff(conflicting_commit_project,
|
115
|
+
conflicting_commit_parent_project, :added, :removed)
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
end
|
@@ -0,0 +1,592 @@
|
|
1
|
+
# Copyright (c) 2020 Lightricks. All rights reserved.
|
2
|
+
# Created by Ben Yohay.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
require "xcodeproj"
|
6
|
+
|
7
|
+
require_relative "utils"
|
8
|
+
require_relative "xcodeproj_extensions"
|
9
|
+
|
10
|
+
module Kintsugi
|
11
|
+
class << self
|
12
|
+
# Applies the change specified by `change` to `project`.
|
13
|
+
#
|
14
|
+
# @param [Xcodeproj::Project] project
|
15
|
+
# Project to which to apply the change.
|
16
|
+
#
|
17
|
+
# @param [Hash] change
|
18
|
+
# Change to apply to `project`. Assumed to be in the format emitted by
|
19
|
+
# Xcodeproj::Differ#project_diff where its `key_1` and `key_2` parameters have values of
|
20
|
+
# `:added` and `:removed` respectively.
|
21
|
+
#
|
22
|
+
# @return [void]
|
23
|
+
def apply_change_to_project(project, change)
|
24
|
+
# We iterate over the main group and project references first because they might create file
|
25
|
+
# or project references that are referenced in other parts.
|
26
|
+
unless change["rootObject"]["mainGroup"].nil?
|
27
|
+
if project.root_object.main_group.nil?
|
28
|
+
puts "Warning: Main group doesn't exist, ignoring changes to it."
|
29
|
+
else
|
30
|
+
apply_change_to_component(project.root_object, "mainGroup",
|
31
|
+
change["rootObject"]["mainGroup"])
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
unless change["rootObject"]["projectReferences"].nil?
|
36
|
+
apply_change_to_component(project.root_object, "projectReferences",
|
37
|
+
change["rootObject"]["projectReferences"])
|
38
|
+
end
|
39
|
+
|
40
|
+
apply_change_to_component(project, "rootObject",
|
41
|
+
change["rootObject"].reject { |key|
|
42
|
+
%w[mainGroup projectReferences].include?(key)
|
43
|
+
})
|
44
|
+
end
|
45
|
+
|
46
|
+
private
|
47
|
+
|
48
|
+
def apply_change_to_component(parent_component, change_name, change)
|
49
|
+
return if change_name == "displayName"
|
50
|
+
|
51
|
+
attribute_name = attribute_name_from_change_name(change_name)
|
52
|
+
if simple_attribute?(parent_component, attribute_name)
|
53
|
+
apply_change_to_simple_attribute(parent_component, attribute_name, change)
|
54
|
+
return
|
55
|
+
end
|
56
|
+
|
57
|
+
if change["isa"]
|
58
|
+
component = replace_component_with_new_type(parent_component, attribute_name, change)
|
59
|
+
change = change_for_component_of_new_type(component, change)
|
60
|
+
else
|
61
|
+
component = child_component(parent_component, change_name)
|
62
|
+
|
63
|
+
if component.nil?
|
64
|
+
add_missing_component_if_valid(parent_component, change_name, change)
|
65
|
+
return
|
66
|
+
end
|
67
|
+
end
|
68
|
+
|
69
|
+
(change[:removed] || []).each do |removed_change|
|
70
|
+
child = child_component(component, removed_change["displayName"])
|
71
|
+
next if child.nil?
|
72
|
+
|
73
|
+
remove_component(child, removed_change)
|
74
|
+
end
|
75
|
+
|
76
|
+
(change[:added] || []).each do |added_change|
|
77
|
+
is_object_list = component.is_a?(Xcodeproj::Project::ObjectList)
|
78
|
+
add_child_to_component(is_object_list ? parent_component : component, added_change)
|
79
|
+
end
|
80
|
+
|
81
|
+
subchanges_of_change(change).each do |subchange_name, subchange|
|
82
|
+
apply_change_to_component(component, subchange_name, subchange)
|
83
|
+
end
|
84
|
+
end
|
85
|
+
|
86
|
+
def subchanges_of_change(change)
|
87
|
+
if change.key?(:diff)
|
88
|
+
change[:diff]
|
89
|
+
else
|
90
|
+
change.reject { |change_name, _| %i[added removed].include?(change_name) }
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def attribute_name_from_change_name(change_name)
|
95
|
+
if change_name == "fileEncoding"
|
96
|
+
change_name.to_sym
|
97
|
+
else
|
98
|
+
Xcodeproj::Project::Object::CaseConverter.convert_to_ruby(change_name)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_missing_component_if_valid(parent_component, change_name, change)
|
103
|
+
if change[:added] && change.compact.count == 1
|
104
|
+
add_child_to_component(parent_component, change[:added])
|
105
|
+
return
|
106
|
+
end
|
107
|
+
|
108
|
+
puts "Warning: Detected change of an object named '#{change_name}' contained in " \
|
109
|
+
"'#{parent_component}' but the object doesn't exist. Ignoring this change."
|
110
|
+
end
|
111
|
+
|
112
|
+
def replace_component_with_new_type(parent_component, name_in_parent_component, change)
|
113
|
+
old_component = parent_component.send(name_in_parent_component)
|
114
|
+
|
115
|
+
new_component = parent_component.project.new(
|
116
|
+
Module.const_get("Xcodeproj::Project::#{change["isa"][:added]}")
|
117
|
+
)
|
118
|
+
|
119
|
+
copy_attributes_to_new_component(old_component, new_component)
|
120
|
+
|
121
|
+
parent_component.send("#{name_in_parent_component}=", new_component)
|
122
|
+
new_component
|
123
|
+
end
|
124
|
+
|
125
|
+
def copy_attributes_to_new_component(old_component, new_component)
|
126
|
+
# The change won't describe the attributes that haven't changed, therefore the attributes
|
127
|
+
# are copied to the new component.
|
128
|
+
old_component.attributes.each do |attribute|
|
129
|
+
next if %i[isa display_name].include?(attribute.name) ||
|
130
|
+
!new_component.respond_to?(attribute.name)
|
131
|
+
|
132
|
+
new_component.send("#{attribute.name}=", old_component.send(attribute.name))
|
133
|
+
end
|
134
|
+
end
|
135
|
+
|
136
|
+
def change_for_component_of_new_type(new_component, change)
|
137
|
+
change.select do |subchange_name, _|
|
138
|
+
next false if subchange_name == "isa"
|
139
|
+
|
140
|
+
attribute_name = attribute_name_from_change_name(subchange_name)
|
141
|
+
new_component.respond_to?(attribute_name)
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
def child_component(component, change_name)
|
146
|
+
if component.is_a?(Xcodeproj::Project::ObjectList)
|
147
|
+
component.find { |child| child.display_name == change_name }
|
148
|
+
else
|
149
|
+
attribute_name = attribute_name_from_change_name(change_name)
|
150
|
+
component.send(attribute_name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def simple_attribute?(component, attribute_name)
|
155
|
+
return false unless component.respond_to?("simple_attributes")
|
156
|
+
|
157
|
+
component.simple_attributes.any? { |attribute| attribute.name == attribute_name }
|
158
|
+
end
|
159
|
+
|
160
|
+
def apply_change_to_simple_attribute(component, attribute_name, change)
|
161
|
+
new_attribute_value =
|
162
|
+
simple_attribute_value_with_change(component.send(attribute_name), change)
|
163
|
+
component.send("#{attribute_name}=", new_attribute_value)
|
164
|
+
end
|
165
|
+
|
166
|
+
def simple_attribute_value_with_change(old_value, change)
|
167
|
+
new_value = nil
|
168
|
+
|
169
|
+
if change.key?(:removed)
|
170
|
+
new_value = apply_removal_to_simple_attribute(old_value, change[:removed])
|
171
|
+
end
|
172
|
+
|
173
|
+
if change.key?(:added)
|
174
|
+
new_value = apply_addition_to_simple_attribute(old_value, change[:added])
|
175
|
+
end
|
176
|
+
|
177
|
+
subchanges_of_change(change).each do |subchange_name, subchange_value|
|
178
|
+
new_value = new_value || old_value || {}
|
179
|
+
new_value[subchange_name] =
|
180
|
+
simple_attribute_value_with_change(old_value[subchange_name], subchange_value)
|
181
|
+
end
|
182
|
+
|
183
|
+
new_value
|
184
|
+
end
|
185
|
+
|
186
|
+
def apply_removal_to_simple_attribute(old_value, change)
|
187
|
+
case change
|
188
|
+
when Array
|
189
|
+
(old_value || []) - change
|
190
|
+
when Hash
|
191
|
+
(old_value || {}).reject do |key, value|
|
192
|
+
if value != change[key]
|
193
|
+
raise "Trying to remove value #{change[key]} of hash with key #{key} but it changed " \
|
194
|
+
"to #{value}."
|
195
|
+
end
|
196
|
+
|
197
|
+
change.key?(key)
|
198
|
+
end
|
199
|
+
when String
|
200
|
+
if old_value != change
|
201
|
+
raise "Value changed from #{old_value} to #{change}."
|
202
|
+
end
|
203
|
+
|
204
|
+
nil
|
205
|
+
when nil
|
206
|
+
nil
|
207
|
+
else
|
208
|
+
raise "Unsupported change #{change} of type #{change.class}"
|
209
|
+
end
|
210
|
+
end
|
211
|
+
|
212
|
+
def apply_addition_to_simple_attribute(old_value, change)
|
213
|
+
case change
|
214
|
+
when Array
|
215
|
+
(old_value || []) + change
|
216
|
+
when Hash
|
217
|
+
old_value ||= {}
|
218
|
+
new_value = old_value.merge(change)
|
219
|
+
|
220
|
+
unless (old_value.to_a - new_value.to_a).empty?
|
221
|
+
raise "New hash #{change} contains values that conflict with old hash #{old_value}"
|
222
|
+
end
|
223
|
+
|
224
|
+
new_value
|
225
|
+
when String
|
226
|
+
change
|
227
|
+
when nil
|
228
|
+
nil
|
229
|
+
else
|
230
|
+
raise "Unsupported change #{change} of type #{change.class}"
|
231
|
+
end
|
232
|
+
end
|
233
|
+
|
234
|
+
def remove_component(component, change)
|
235
|
+
if component.to_tree_hash != change
|
236
|
+
raise "Trying to remove an object that changed since then. This is considered a conflict " \
|
237
|
+
"that should be resolved manually. Name of the object is: '#{component.display_name}'"
|
238
|
+
end
|
239
|
+
|
240
|
+
if change["isa"] == "PBXFileReference"
|
241
|
+
remove_build_files_of_file_reference(component, change)
|
242
|
+
end
|
243
|
+
|
244
|
+
component.remove_from_project
|
245
|
+
end
|
246
|
+
|
247
|
+
def remove_build_files_of_file_reference(file_reference, change)
|
248
|
+
# Since the build file's display name depends on the file reference, removing the file
|
249
|
+
# reference before removing it will change the build file's display name which will not be
|
250
|
+
# detected when trying to remove the build file. Therefore, the build files that depend on
|
251
|
+
# the file reference are removed prior to removing the file reference.
|
252
|
+
file_reference.build_files.each do |build_file|
|
253
|
+
build_file.referrers.each do |referrer|
|
254
|
+
referrer.remove_build_file(build_file)
|
255
|
+
end
|
256
|
+
end
|
257
|
+
end
|
258
|
+
|
259
|
+
def add_child_to_component(component, change)
|
260
|
+
if change["ProjectRef"] && change["ProductGroup"]
|
261
|
+
add_subproject_reference(component, change)
|
262
|
+
return
|
263
|
+
end
|
264
|
+
|
265
|
+
case change["isa"]
|
266
|
+
when "PBXNativeTarget"
|
267
|
+
add_target(component, change)
|
268
|
+
when "PBXFileReference"
|
269
|
+
add_file_reference(component, change)
|
270
|
+
when "PBXGroup"
|
271
|
+
add_group(component, change)
|
272
|
+
when "PBXContainerItemProxy"
|
273
|
+
add_container_item_proxy(component, change)
|
274
|
+
when "PBXTargetDependency"
|
275
|
+
add_target_dependency(component, change)
|
276
|
+
when "PBXBuildFile"
|
277
|
+
add_build_file(component, change)
|
278
|
+
when "XCConfigurationList"
|
279
|
+
add_build_configuration_list(component, change)
|
280
|
+
when "XCBuildConfiguration"
|
281
|
+
add_build_configuration(component, change)
|
282
|
+
when "PBXHeadersBuildPhase"
|
283
|
+
add_headers_build_phase(component, change)
|
284
|
+
when "PBXSourcesBuildPhase"
|
285
|
+
add_sources_build_phase(component, change)
|
286
|
+
when "PBXCopyFilesBuildPhase"
|
287
|
+
add_copy_files_build_phase(component, change)
|
288
|
+
when "PBXShellScriptBuildPhase"
|
289
|
+
add_shell_script_build_phase(component, change)
|
290
|
+
when "PBXFrameworksBuildPhase"
|
291
|
+
add_frameworks_build_phase(component, change)
|
292
|
+
when "PBXResourcesBuildPhase"
|
293
|
+
add_resources_build_phase(component, change)
|
294
|
+
when "PBXBuildRule"
|
295
|
+
add_build_rule(component, change)
|
296
|
+
when "PBXVariantGroup"
|
297
|
+
add_variant_group(component, change)
|
298
|
+
when "PBXReferenceProxy"
|
299
|
+
add_reference_proxy(component, change)
|
300
|
+
else
|
301
|
+
raise "Trying to add unsupported component type #{change["isa"]}. Full component change " \
|
302
|
+
"is: #{change}"
|
303
|
+
end
|
304
|
+
end
|
305
|
+
|
306
|
+
def add_reference_proxy(containing_component, change)
|
307
|
+
case containing_component
|
308
|
+
when Xcodeproj::Project::PBXBuildFile
|
309
|
+
containing_component.file_ref = find_file(containing_component.project, change)
|
310
|
+
when Xcodeproj::Project::PBXGroup
|
311
|
+
reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
|
312
|
+
containing_component << reference_proxy
|
313
|
+
add_attributes_to_component(reference_proxy, change)
|
314
|
+
else
|
315
|
+
raise "Trying to add reference proxy to an unsupported component type " \
|
316
|
+
"#{containing_component.isa}. Change is: #{change}"
|
317
|
+
end
|
318
|
+
end
|
319
|
+
|
320
|
+
def add_variant_group(containing_component, change)
|
321
|
+
case containing_component
|
322
|
+
when Xcodeproj::Project::PBXBuildFile
|
323
|
+
containing_component.file_ref =
|
324
|
+
find_variant_group(containing_component.project, change["displayName"])
|
325
|
+
when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
|
326
|
+
variant_group = containing_component.project.new(Xcodeproj::Project::PBXVariantGroup)
|
327
|
+
containing_component.children << variant_group
|
328
|
+
add_attributes_to_component(variant_group, change)
|
329
|
+
else
|
330
|
+
raise "Trying to add variant group to an unsupported component type " \
|
331
|
+
"#{containing_component.isa}. Change is: #{change}"
|
332
|
+
end
|
333
|
+
end
|
334
|
+
|
335
|
+
def add_build_rule(target, change)
|
336
|
+
build_rule = target.project.new(Xcodeproj::Project::PBXBuildRule)
|
337
|
+
target.build_rules << build_rule
|
338
|
+
add_attributes_to_component(build_rule, change)
|
339
|
+
end
|
340
|
+
|
341
|
+
def add_shell_script_build_phase(target, change)
|
342
|
+
build_phase = target.new_shell_script_build_phase(change["displayName"])
|
343
|
+
add_attributes_to_component(build_phase, change)
|
344
|
+
end
|
345
|
+
|
346
|
+
def add_headers_build_phase(target, change)
|
347
|
+
add_attributes_to_component(target.headers_build_phase, change)
|
348
|
+
end
|
349
|
+
|
350
|
+
def add_sources_build_phase(target, change)
|
351
|
+
add_attributes_to_component(target.source_build_phase, change)
|
352
|
+
end
|
353
|
+
|
354
|
+
def add_frameworks_build_phase(target, change)
|
355
|
+
add_attributes_to_component(target.frameworks_build_phase, change)
|
356
|
+
end
|
357
|
+
|
358
|
+
def add_resources_build_phase(target, change)
|
359
|
+
add_attributes_to_component(target.resources_build_phase, change)
|
360
|
+
end
|
361
|
+
|
362
|
+
def add_copy_files_build_phase(target, change)
|
363
|
+
copy_files_phase_name = change["displayName"] == "CopyFiles" ? nil : change["displayName"]
|
364
|
+
copy_files_phase = target.new_copy_files_build_phase(copy_files_phase_name)
|
365
|
+
|
366
|
+
add_attributes_to_component(copy_files_phase, change)
|
367
|
+
end
|
368
|
+
|
369
|
+
def add_build_configuration_list(target, change)
|
370
|
+
target.build_configuration_list = target.project.new(Xcodeproj::Project::XCConfigurationList)
|
371
|
+
add_attributes_to_component(target.build_configuration_list, change)
|
372
|
+
end
|
373
|
+
|
374
|
+
def add_build_configuration(configuration_list, change)
|
375
|
+
build_configuration = configuration_list.project.new(Xcodeproj::Project::XCBuildConfiguration)
|
376
|
+
configuration_list.build_configurations << build_configuration
|
377
|
+
add_attributes_to_component(build_configuration, change)
|
378
|
+
end
|
379
|
+
|
380
|
+
def add_build_file(build_phase, change)
|
381
|
+
if change["fileRef"].nil?
|
382
|
+
puts "Warning: Trying to add a build file without any file reference to build phase " \
|
383
|
+
"'#{build_phase}'"
|
384
|
+
return
|
385
|
+
end
|
386
|
+
|
387
|
+
build_file = build_phase.project.new(Xcodeproj::Project::PBXBuildFile)
|
388
|
+
build_phase.files << build_file
|
389
|
+
add_attributes_to_component(build_file, change)
|
390
|
+
end
|
391
|
+
|
392
|
+
def find_variant_group(project, display_name)
|
393
|
+
project.objects.find do |object|
|
394
|
+
object.isa == "PBXVariantGroup" && object.display_name == display_name
|
395
|
+
end
|
396
|
+
end
|
397
|
+
|
398
|
+
def add_target_dependency(target, change)
|
399
|
+
target_dependency = find_target(target.project, change["displayName"])
|
400
|
+
|
401
|
+
if target_dependency
|
402
|
+
target.add_dependency(target_dependency)
|
403
|
+
return
|
404
|
+
end
|
405
|
+
|
406
|
+
target_dependency = target.project.new(Xcodeproj::Project::PBXTargetDependency)
|
407
|
+
|
408
|
+
target.dependencies << target_dependency
|
409
|
+
add_attributes_to_component(target_dependency, change)
|
410
|
+
end
|
411
|
+
|
412
|
+
def find_target(project, display_name)
|
413
|
+
project.targets.find { |target| target.display_name == display_name }
|
414
|
+
end
|
415
|
+
|
416
|
+
def add_container_item_proxy(component, change)
|
417
|
+
container_proxy = component.project.new(Xcodeproj::Project::PBXContainerItemProxy)
|
418
|
+
container_proxy.container_portal = find_containing_project_uuid(component.project, change)
|
419
|
+
|
420
|
+
case component.isa
|
421
|
+
when "PBXTargetDependency"
|
422
|
+
component.target_proxy = container_proxy
|
423
|
+
when "PBXReferenceProxy"
|
424
|
+
component.remote_ref = container_proxy
|
425
|
+
else
|
426
|
+
raise "Trying to add container item proxy to an unsupported component type " \
|
427
|
+
"#{containing_component.isa}. Change is: #{change}"
|
428
|
+
end
|
429
|
+
add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
|
430
|
+
end
|
431
|
+
|
432
|
+
def find_containing_project_uuid(project, container_item_proxy_change)
|
433
|
+
if project.objects_by_uuid[container_item_proxy_change["containerPortal"]]
|
434
|
+
return container_item_proxy_change["containerPortal"]
|
435
|
+
end
|
436
|
+
|
437
|
+
# The `containerPortal` from `container_item_proxy_change` might not be relevant, since when a
|
438
|
+
# project is added its UUID is generated. Instead, existing container item proxies are
|
439
|
+
# searched, until one that has the same remote info as the one in
|
440
|
+
# `container_item_proxy_change` is found.
|
441
|
+
container_item_proxies =
|
442
|
+
project.root_object.project_references.map do |project_ref_and_products|
|
443
|
+
project_ref_and_products[:project_ref].proxy_containers.find do |container_proxy|
|
444
|
+
container_proxy.remote_info == container_item_proxy_change["remoteInfo"]
|
445
|
+
end
|
446
|
+
end.compact
|
447
|
+
|
448
|
+
if container_item_proxies.length > 1
|
449
|
+
puts "Debug: Found more than one potential dependency with name " \
|
450
|
+
"'#{container_item_proxy_change["remoteInfo"]}'. Using the first one."
|
451
|
+
elsif container_item_proxies.empty?
|
452
|
+
puts "Warning: No container portal was found for dependency with name " \
|
453
|
+
"'#{container_item_proxy_change["remoteInfo"]}'."
|
454
|
+
return
|
455
|
+
end
|
456
|
+
|
457
|
+
container_item_proxies.first.container_portal
|
458
|
+
end
|
459
|
+
|
460
|
+
def add_subproject_reference(root_object, project_reference_change)
|
461
|
+
subproject_reference = find_file(root_object.project, project_reference_change["ProjectRef"])
|
462
|
+
|
463
|
+
attribute =
|
464
|
+
Xcodeproj::Project::PBXProject.references_by_keys_attributes
|
465
|
+
.find { |attrb| attrb.name == :project_references }
|
466
|
+
project_reference = Xcodeproj::Project::ObjectDictionary.new(attribute, root_object)
|
467
|
+
project_reference[:project_ref] = subproject_reference
|
468
|
+
root_object.project_references << project_reference
|
469
|
+
|
470
|
+
updated_project_reference_change =
|
471
|
+
change_with_updated_subproject_uuid(project_reference_change, subproject_reference.uuid)
|
472
|
+
add_attributes_to_component(project_reference, updated_project_reference_change,
|
473
|
+
ignore_keys: ["ProjectRef"])
|
474
|
+
end
|
475
|
+
|
476
|
+
def change_with_updated_subproject_uuid(change, subproject_reference_uuid)
|
477
|
+
new_change = change.deep_clone
|
478
|
+
new_change["ProductGroup"]["children"].map do |product_reference_change|
|
479
|
+
product_reference_change["remoteRef"]["containerPortal"] = subproject_reference_uuid
|
480
|
+
product_reference_change
|
481
|
+
end
|
482
|
+
new_change
|
483
|
+
end
|
484
|
+
|
485
|
+
def add_target(root_object, change)
|
486
|
+
target = root_object.project.new(Xcodeproj::Project::PBXNativeTarget)
|
487
|
+
root_object.project.targets << target
|
488
|
+
add_attributes_to_component(target, change)
|
489
|
+
end
|
490
|
+
|
491
|
+
def add_file_reference(containing_component, change)
|
492
|
+
# base configuration reference and product reference always reference a file that exists
|
493
|
+
# inside a group, therefore in these cases the file is searched for.
|
494
|
+
# In the case of group and variant group, the file can't exist in another group, therefore a
|
495
|
+
# new file reference is always created.
|
496
|
+
case containing_component
|
497
|
+
when Xcodeproj::Project::XCBuildConfiguration
|
498
|
+
containing_component.base_configuration_reference =
|
499
|
+
find_file(containing_component.project, change)
|
500
|
+
when Xcodeproj::Project::PBXNativeTarget
|
501
|
+
containing_component.product_reference = find_file(containing_component.project, change)
|
502
|
+
when Xcodeproj::Project::PBXBuildFile
|
503
|
+
containing_component.file_ref = find_file(containing_component.project, change)
|
504
|
+
when Xcodeproj::Project::PBXGroup, Xcodeproj::Project::PBXVariantGroup
|
505
|
+
file_reference = containing_component.project.new(Xcodeproj::Project::PBXFileReference)
|
506
|
+
containing_component.children << file_reference
|
507
|
+
|
508
|
+
# For some reason, `include_in_index` is set to `1` by default.
|
509
|
+
file_reference.include_in_index = nil
|
510
|
+
add_attributes_to_component(file_reference, change)
|
511
|
+
else
|
512
|
+
raise "Trying to add file reference to an unsupported component type " \
|
513
|
+
"#{containing_component.isa}. Change is: #{change}"
|
514
|
+
end
|
515
|
+
end
|
516
|
+
|
517
|
+
def add_group(containing_component, change)
|
518
|
+
case containing_component
|
519
|
+
when Xcodeproj::Project::ObjectDictionary
|
520
|
+
# It is assumed that an `ObjectDictionary` always represents a project reference.
|
521
|
+
new_group = containing_component[:project_ref].project.new(Xcodeproj::Project::PBXGroup)
|
522
|
+
containing_component[:product_group] = new_group
|
523
|
+
when Xcodeproj::Project::PBXGroup
|
524
|
+
new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
|
525
|
+
containing_component.children << new_group
|
526
|
+
else
|
527
|
+
raise "Trying to add group to an unsupported component type #{containing_component.isa}. " \
|
528
|
+
"Change is: #{change}"
|
529
|
+
end
|
530
|
+
|
531
|
+
add_attributes_to_component(new_group, change)
|
532
|
+
end
|
533
|
+
|
534
|
+
def add_attributes_to_component(component, change, ignore_keys: [])
|
535
|
+
change.each do |change_name, change_value|
|
536
|
+
next if (%w[isa displayName] + ignore_keys).include?(change_name)
|
537
|
+
|
538
|
+
attribute_name = attribute_name_from_change_name(change_name)
|
539
|
+
if simple_attribute?(component, attribute_name)
|
540
|
+
apply_change_to_simple_attribute(component, attribute_name, {added: change_value})
|
541
|
+
next
|
542
|
+
end
|
543
|
+
|
544
|
+
case change_value
|
545
|
+
when Hash
|
546
|
+
add_child_to_component(component, change_value)
|
547
|
+
when Array
|
548
|
+
change_value.each do |added_attribute_element|
|
549
|
+
add_child_to_component(component, added_attribute_element)
|
550
|
+
end
|
551
|
+
else
|
552
|
+
raise "Trying to add attribute of unsupported type '#{change_value.class}' to " \
|
553
|
+
"object #{component}. Attribute name is '#{change_name}'"
|
554
|
+
end
|
555
|
+
end
|
556
|
+
end
|
557
|
+
|
558
|
+
def find_file(project, file_reference_change)
|
559
|
+
case file_reference_change["isa"]
|
560
|
+
when "PBXFileReference"
|
561
|
+
project.files.find do |file_reference|
|
562
|
+
next file_reference.path == file_reference_change["path"]
|
563
|
+
end
|
564
|
+
when "PBXReferenceProxy"
|
565
|
+
find_reference_proxy(project, file_reference_change["remoteRef"])
|
566
|
+
else
|
567
|
+
raise "Unsupported file reference change of type #{file_reference["isa"]}."
|
568
|
+
end
|
569
|
+
end
|
570
|
+
|
571
|
+
def find_reference_proxy(project, container_item_proxy_change)
|
572
|
+
reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
|
573
|
+
project_ref_and_products[:product_group].children.find do |product|
|
574
|
+
product.remote_ref.remote_global_id_string ==
|
575
|
+
container_item_proxy_change["remoteGlobalIDString"] &&
|
576
|
+
product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
|
577
|
+
end
|
578
|
+
end.compact
|
579
|
+
|
580
|
+
if reference_proxies.length > 1
|
581
|
+
puts "Debug: Found more than one matching reference proxy with name " \
|
582
|
+
"'#{container_item_proxy_change["remoteInfo"]}'. Using the first one."
|
583
|
+
elsif reference_proxies.empty?
|
584
|
+
puts "Warning: No reference proxy was found for name " \
|
585
|
+
"'#{container_item_proxy_change["remoteInfo"]}'."
|
586
|
+
return
|
587
|
+
end
|
588
|
+
|
589
|
+
reference_proxies.first
|
590
|
+
end
|
591
|
+
end
|
592
|
+
end
|