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.
@@ -0,0 +1,37 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ class Array
6
+ # Provides a deep clone of `self`
7
+ #
8
+ # @return [Array]
9
+ def deep_clone
10
+ map do |value|
11
+ begin
12
+ value.deep_clone
13
+ rescue NoMethodError
14
+ value.clone
15
+ end
16
+ rescue NoMethodError
17
+ value
18
+ end
19
+ end
20
+ end
21
+
22
+ class Hash
23
+ # Provides a deep clone of `self`
24
+ #
25
+ # @return [Hash]
26
+ def deep_clone
27
+ transform_values do |value|
28
+ begin
29
+ value.deep_clone
30
+ rescue NoMethodError
31
+ value.clone
32
+ end
33
+ rescue NoMethodError
34
+ value
35
+ end
36
+ end
37
+ end
@@ -0,0 +1,31 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "xcodeproj"
6
+
7
+ module Xcodeproj
8
+ class Project
9
+ # Extends `ObjectDictionary` to act like an `Object` if `self` repreresents a project reference.
10
+ class ObjectDictionary
11
+ @@old_to_tree_hash = instance_method(:to_tree_hash)
12
+
13
+ def to_tree_hash
14
+ result = @@old_to_tree_hash.bind(self).call
15
+ self[:project_ref] ? result.merge("displayName" => display_name) : result
16
+ end
17
+
18
+ def display_name
19
+ project_ref.display_name
20
+ end
21
+
22
+ def product_group
23
+ self[:product_group]
24
+ end
25
+
26
+ def project_ref
27
+ self[:project_ref]
28
+ end
29
+ end
30
+ end
31
+ end
data/logo/kintsugi.png ADDED
Binary file
@@ -0,0 +1,63 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ class Array
6
+ def delete_value_recursive!(value_to_delete)
7
+ each do |value|
8
+ if value == value_to_delete
9
+ delete(value)
10
+ end
11
+
12
+ if value.instance_of?(Hash)
13
+ value.delete_key_recursive!(value_to_delete)
14
+ elsif value.instance_of?(Array)
15
+ value.delete_value_recursive!(value_to_delete)
16
+ end
17
+ end
18
+ end
19
+ end
20
+
21
+ class Hash
22
+ def delete_key_recursive!(key)
23
+ if key?(key)
24
+ delete(key)
25
+ end
26
+
27
+ each do |_, value|
28
+ if value.instance_of?(Hash)
29
+ value.delete_key_recursive!(key)
30
+ elsif value.instance_of?(Array)
31
+ value.delete_value_recursive!(key)
32
+ end
33
+ end
34
+ end
35
+ end
36
+
37
+ RSpec::Matchers.define :be_equivalent_to_project do |expected_project, ignore_keys: []|
38
+ def _calculate_project_diff(expected_project, actual_project, ignore_keys)
39
+ expected_project_hash = expected_project.to_tree_hash.dup
40
+ actual_project_hash = actual_project.to_tree_hash.dup
41
+
42
+ ignore_keys.each do |ignored_key|
43
+ expected_project_hash.delete_key_recursive!(ignored_key)
44
+ actual_project_hash.delete_key_recursive!(ignored_key)
45
+ end
46
+
47
+ diff =
48
+ Xcodeproj::Differ.project_diff(expected_project_hash, actual_project_hash, :expected, :actual)
49
+ diff["rootObject"].delete("displayName")
50
+ diff
51
+ end
52
+
53
+ match do |actual_project|
54
+ _calculate_project_diff(expected_project, actual_project, ignore_keys) == {"rootObject" => {}}
55
+ end
56
+
57
+ failure_message do |actual_project|
58
+ diff = _calculate_project_diff(expected_project, actual_project, ignore_keys)
59
+
60
+ "expected #{actual} to be equivalent to project #{expected}, their difference is:
61
+ #{JSON.pretty_generate(diff)}"
62
+ end
63
+ end
@@ -0,0 +1,885 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "json"
6
+ require "rspec"
7
+ require "tempfile"
8
+ require "tmpdir"
9
+
10
+ require "kintsugi/apply_change_to_project"
11
+
12
+ require_relative "be_equivalent_to_project"
13
+
14
+ describe Kintsugi, :apply_change_to_project do
15
+ let(:temporary_directories_paths) { [] }
16
+ let(:base_project_path) { make_temp_directory("base", ".xcodeproj") }
17
+ let(:base_project) { Xcodeproj::Project.new(base_project_path) }
18
+
19
+ before do
20
+ base_project.save
21
+ end
22
+
23
+ after do
24
+ temporary_directories_paths.each do |directory_path|
25
+ FileUtils.remove_entry(directory_path)
26
+ end
27
+ end
28
+
29
+ it "adds new target" do
30
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
31
+ theirs_project.new_target("com.apple.product-type.library.static", "foo", :ios)
32
+
33
+ changes_to_apply = get_diff(theirs_project, base_project)
34
+
35
+ described_class.apply_change_to_project(base_project, changes_to_apply)
36
+ base_project.save
37
+
38
+ expect(base_project).to be_equivalent_to_project(theirs_project)
39
+ end
40
+
41
+ it "adds new subproject" do
42
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
43
+ add_new_subproject_to_project(theirs_project, "foo", "foo")
44
+
45
+ changes_to_apply = get_diff(theirs_project, base_project)
46
+
47
+ described_class.apply_change_to_project(base_project, changes_to_apply)
48
+ base_project.save
49
+
50
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
51
+ end
52
+
53
+ # Checks that the order the changes are applied in is correct.
54
+ it "adds new subproject and reference to its framework" do
55
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
56
+ add_new_subproject_to_project(theirs_project, "foo", "foo")
57
+
58
+ target = theirs_project.new_target("com.apple.product-type.library.static", "foo", :ios)
59
+ target.frameworks_build_phase.add_file_reference(
60
+ theirs_project.root_object.project_references[0][:product_group].children[0]
61
+ )
62
+
63
+ changes_to_apply = get_diff(theirs_project, base_project)
64
+
65
+ described_class.apply_change_to_project(base_project, changes_to_apply)
66
+ base_project.save
67
+
68
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
69
+ end
70
+
71
+ describe "file related changes" do
72
+ let(:filepath) { "foo" }
73
+
74
+ before do
75
+ base_project.main_group.new_reference(filepath)
76
+ base_project.save
77
+ end
78
+
79
+ it "moves file to another group" do
80
+ base_project.main_group.find_subpath("new_group", true)
81
+ base_project.save
82
+
83
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
84
+ new_group = theirs_project.main_group.find_subpath("new_group")
85
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
86
+ file_reference.move(new_group)
87
+
88
+ changes_to_apply = get_diff(theirs_project, base_project)
89
+
90
+ described_class.apply_change_to_project(base_project, changes_to_apply)
91
+ base_project.save
92
+
93
+ expect(base_project).to be_equivalent_to_project(theirs_project)
94
+ end
95
+
96
+ it "adds file to new group" do
97
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
98
+
99
+ theirs_project.main_group.find_subpath("new_group", true).new_reference(filepath)
100
+
101
+ changes_to_apply = get_diff(theirs_project, base_project)
102
+
103
+ described_class.apply_change_to_project(base_project, changes_to_apply)
104
+ base_project.save
105
+
106
+ expect(base_project).to be_equivalent_to_project(theirs_project)
107
+ end
108
+
109
+ it "adds file with include in index and last known file type as nil" do
110
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
111
+ file_reference = theirs_project.main_group.new_reference("#{filepath}.h")
112
+ file_reference.include_in_index = nil
113
+ file_reference.last_known_file_type = nil
114
+
115
+ changes_to_apply = get_diff(theirs_project, base_project)
116
+
117
+ described_class.apply_change_to_project(base_project, changes_to_apply)
118
+ base_project.save
119
+
120
+ expect(base_project).to be_equivalent_to_project(theirs_project)
121
+ end
122
+
123
+ it "renames file" do
124
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
125
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
126
+ file_reference.path = "newFoo"
127
+
128
+ changes_to_apply = get_diff(theirs_project, base_project)
129
+
130
+ described_class.apply_change_to_project(base_project, changes_to_apply)
131
+
132
+ expect(base_project).to be_equivalent_to_project(theirs_project)
133
+ end
134
+
135
+ it "removes file" do
136
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
137
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
138
+
139
+ changes_to_apply = get_diff(theirs_project, base_project)
140
+
141
+ described_class.apply_change_to_project(base_project, changes_to_apply)
142
+ base_project.save
143
+
144
+ expect(base_project).to be_equivalent_to_project(theirs_project)
145
+ end
146
+
147
+ it "removes build files of a removed file" do
148
+ target = base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
149
+ target.source_build_phase.add_file_reference(
150
+ base_project.main_group.find_file_by_path(filepath)
151
+ )
152
+ base_project.save
153
+
154
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
155
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
156
+ file_reference.build_files.each do |build_file|
157
+ build_file.referrers.each do |referrer|
158
+ referrer.remove_build_file(build_file)
159
+ end
160
+ end
161
+ file_reference.remove_from_project
162
+
163
+ changes_to_apply = get_diff(theirs_project, base_project)
164
+
165
+ described_class.apply_change_to_project(base_project, changes_to_apply)
166
+ base_project.save
167
+
168
+ expect(base_project).to be_equivalent_to_project(theirs_project)
169
+ end
170
+
171
+ it "adds file inside a group that has a path on filesystem" do
172
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
173
+
174
+ new_group = theirs_project.main_group.find_subpath("new_group", true)
175
+ new_group.path = "some_path"
176
+ new_group.name = nil
177
+ new_group.new_reference(filepath)
178
+
179
+ changes_to_apply = get_diff(theirs_project, base_project)
180
+
181
+ described_class.apply_change_to_project(base_project, changes_to_apply)
182
+ base_project.save
183
+
184
+ expect(base_project).to be_equivalent_to_project(theirs_project)
185
+ end
186
+
187
+ it "handles subfile changes" do
188
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
189
+
190
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
191
+ theirs_project.main_group.find_file_by_path(filepath).include_in_index = "0"
192
+ theirs_project.main_group.find_file_by_path(filepath).fileEncoding = "4"
193
+
194
+ changes_to_apply = get_diff(theirs_project, base_project)
195
+
196
+ described_class.apply_change_to_project(base_project, changes_to_apply)
197
+ base_project.save
198
+
199
+ expect(base_project).to be_equivalent_to_project(theirs_project)
200
+ end
201
+
202
+ it "handles moved file to an existing group with a different path on filesystem" do
203
+ base_project.main_group.find_subpath("new_group", true).path = "some_path"
204
+
205
+ base_project.save
206
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
207
+
208
+ new_group = theirs_project.main_group.find_subpath("new_group")
209
+
210
+ theirs_project.main_group.find_file_by_path(filepath).move(new_group)
211
+
212
+ changes_to_apply = get_diff(theirs_project, base_project)
213
+
214
+ described_class.apply_change_to_project(base_project, changes_to_apply)
215
+ base_project.save
216
+
217
+ expect(base_project).to be_equivalent_to_project(theirs_project)
218
+ end
219
+
220
+ describe "dealing with unexpected change" do
221
+ it "ignores change to a file whose containing group doesn't exist" do
222
+ ours_project = create_copy_of_project(base_project.path, "ours")
223
+ ours_project.main_group.remove_from_project
224
+ ours_project.save
225
+
226
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
227
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
228
+
229
+ changes_to_apply = get_diff(theirs_project, base_project)
230
+
231
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
232
+
233
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
234
+ ours_project.save
235
+
236
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
237
+ end
238
+
239
+ it "ignores change to a file that doesn't exist" do
240
+ ours_project = create_copy_of_project(base_project.path, "ours")
241
+ ours_project.main_group.find_file_by_path(filepath).remove_from_project
242
+ ours_project.save
243
+
244
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
245
+ theirs_project.main_group.find_file_by_path(filepath).explicit_file_type = "bar"
246
+
247
+ changes_to_apply = get_diff(theirs_project, base_project)
248
+
249
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
250
+
251
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
252
+ ours_project.save
253
+
254
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
255
+ end
256
+
257
+ it "ignores removal of a file whose group doesn't exist" do
258
+ ours_project = create_copy_of_project(base_project.path, "ours")
259
+ ours_project.main_group.remove_from_project
260
+ ours_project.save
261
+
262
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
263
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
264
+
265
+ changes_to_apply = get_diff(theirs_project, base_project)
266
+
267
+ ours_project_before_applying_changes = create_copy_of_project(ours_project.path, "ours")
268
+
269
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
270
+ ours_project.save
271
+
272
+ expect(ours_project).to be_equivalent_to_project(ours_project_before_applying_changes)
273
+ end
274
+
275
+ it "ignores removal of non-existent file" do
276
+ ours_project = create_copy_of_project(base_project.path, "ours")
277
+ ours_project.main_group.find_file_by_path(filepath).remove_from_project
278
+
279
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
280
+ theirs_project.main_group.find_file_by_path(filepath).remove_from_project
281
+
282
+ changes_to_apply = get_diff(theirs_project, base_project)
283
+
284
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
285
+ ours_project.save
286
+
287
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
288
+ end
289
+ end
290
+ end
291
+
292
+ describe "target related changes" do
293
+ let!(:target) { base_project.new_target("com.apple.product-type.library.static", "foo", :ios) }
294
+
295
+ before do
296
+ base_project.save
297
+ end
298
+
299
+ it "changes framework from file reference to reference proxy" do
300
+ framework_filename = "baz"
301
+
302
+ file_reference = base_project.main_group.new_reference(framework_filename)
303
+ base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
304
+
305
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
306
+ base_project.save
307
+
308
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
309
+
310
+ build_phase = theirs_project.targets[0].frameworks_build_phase
311
+ build_phase.files[0].remove_from_project
312
+ build_phase.add_file_reference(
313
+ theirs_project.root_object.project_references[0][:product_group].children[0]
314
+ )
315
+
316
+ changes_to_apply = get_diff(theirs_project, base_project)
317
+
318
+ described_class.apply_change_to_project(base_project, changes_to_apply)
319
+ base_project.save
320
+
321
+ expect(base_project).to be_equivalent_to_project(theirs_project)
322
+ end
323
+
324
+ it "changes framework from reference proxy to file reference" do
325
+ framework_filename = "baz"
326
+
327
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
328
+ base_project.targets[0].frameworks_build_phase.add_file_reference(
329
+ base_project.root_object.project_references[0][:product_group].children[0]
330
+ )
331
+
332
+ base_project.save
333
+
334
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
335
+
336
+ file_reference = theirs_project.main_group.new_reference(framework_filename)
337
+ build_phase = theirs_project.targets[0].frameworks_build_phase
338
+ build_phase.files[-1].remove_from_project
339
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
340
+
341
+ changes_to_apply = get_diff(theirs_project, base_project)
342
+
343
+ described_class.apply_change_to_project(base_project, changes_to_apply)
344
+ base_project.save
345
+
346
+ expect(base_project).to be_equivalent_to_project(theirs_project)
347
+ end
348
+
349
+ it "adds remote ref to reference proxy" do
350
+ framework_filename = "baz"
351
+
352
+ add_new_subproject_to_project(base_project, "subproj", framework_filename)
353
+ build_file = base_project.targets[0].frameworks_build_phase.add_file_reference(
354
+ base_project.root_object.project_references[0][:product_group].children[0]
355
+ )
356
+ container_proxy = build_file.file_ref.remote_ref
357
+ build_file.file_ref.remote_ref = nil
358
+
359
+ base_project.save
360
+
361
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
362
+
363
+ theirs_project.targets[0].frameworks_build_phase.files[-1].file_ref.remote_ref =
364
+ container_proxy
365
+
366
+ changes_to_apply = get_diff(theirs_project, base_project)
367
+
368
+ changes_to_apply["rootObject"].delete("projectReferences")
369
+
370
+ described_class.apply_change_to_project(base_project, changes_to_apply)
371
+ base_project.save
372
+
373
+ expect(base_project).to be_equivalent_to_project(theirs_project)
374
+ end
375
+
376
+ it "adds subproject target and adds reference to it" do
377
+ framework_filename = "baz"
378
+ subproject = add_new_subproject_to_project(base_project, "subproj", framework_filename)
379
+ base_project.save
380
+
381
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
382
+
383
+ subproject.new_target("com.apple.product-type.library.static", "bari", :ios)
384
+
385
+ theirs_project.root_object.project_references[0][:product_group] <<
386
+ create_reference_proxy_from_product_reference(theirs_project,
387
+ theirs_project.root_object.project_references[0][:project_ref],
388
+ subproject.products_group.files[-1])
389
+
390
+ build_phase = theirs_project.targets[0].frameworks_build_phase
391
+ build_phase.add_file_reference(
392
+ theirs_project.root_object.project_references[0][:product_group].children[-1]
393
+ )
394
+
395
+ changes_to_apply = get_diff(theirs_project, base_project)
396
+
397
+ described_class.apply_change_to_project(base_project, changes_to_apply)
398
+ base_project.save
399
+
400
+ expect(base_project).to be_equivalent_to_project(theirs_project)
401
+ end
402
+
403
+ it "adds new build file" do
404
+ base_project.main_group.new_reference("bar")
405
+ base_project.save
406
+
407
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
408
+
409
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
410
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
411
+
412
+ changes_to_apply = get_diff(theirs_project, base_project)
413
+
414
+ described_class.apply_change_to_project(base_project, changes_to_apply)
415
+ base_project.save
416
+
417
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
418
+ end
419
+
420
+ it "adds file reference to build file" do
421
+ file_reference = base_project.main_group.new_reference("bar")
422
+
423
+ build_file = base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
424
+ build_file.file_ref = nil
425
+
426
+ base_project.save
427
+
428
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
429
+
430
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
431
+ theirs_project.targets[0].frameworks_build_phase.files[-1].file_ref = file_reference
432
+
433
+ changes_to_apply = get_diff(theirs_project, base_project)
434
+
435
+ described_class.apply_change_to_project(base_project, changes_to_apply)
436
+ base_project.save
437
+
438
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
439
+ end
440
+
441
+ it "ignores build file without file reference" do
442
+ base_project.main_group.new_reference("bar")
443
+ base_project.save
444
+
445
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
446
+
447
+ file_reference = theirs_project.main_group.files.find { |file| file.display_name == "bar" }
448
+ build_file =
449
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
450
+ build_file.file_ref = nil
451
+
452
+ changes_to_apply = get_diff(theirs_project, base_project)
453
+
454
+ other_project = create_copy_of_project(base_project.path, "other")
455
+ described_class.apply_change_to_project(other_project, changes_to_apply)
456
+ other_project.save
457
+
458
+ expect(other_project).to be_equivalent_to_project(base_project)
459
+ end
460
+
461
+ it "adds new build rule" do
462
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
463
+
464
+ build_rule = theirs_project.new(Xcodeproj::Project::PBXBuildRule)
465
+ build_rule.compiler_spec = "com.apple.compilers.proxy.script"
466
+ build_rule.file_type = "pattern.proxy"
467
+ build_rule.file_patterns = "*.json"
468
+ build_rule.is_editable = "1"
469
+ build_rule.input_files = [
470
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.json"
471
+ ]
472
+ build_rule.output_files = [
473
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.h",
474
+ "$(DERIVED_FILE_DIR)/$(arch)/${INPUT_FILE_BASE}.mm"
475
+ ]
476
+ build_rule.script = "foo"
477
+ theirs_project.targets[0].build_rules << build_rule
478
+
479
+ changes_to_apply = get_diff(theirs_project, base_project)
480
+
481
+ described_class.apply_change_to_project(base_project, changes_to_apply)
482
+ base_project.save
483
+
484
+ expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
485
+ end
486
+
487
+ it "adds new build setting" do
488
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
489
+
490
+ theirs_project.targets[0].build_configurations.each do |configuration|
491
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
492
+ "$(SRCROOT)/../Foo",
493
+ "$(SRCROOT)/../Bar"
494
+ ]
495
+ end
496
+
497
+ changes_to_apply = get_diff(theirs_project, base_project)
498
+
499
+ described_class.apply_change_to_project(base_project, changes_to_apply)
500
+ base_project.save
501
+
502
+ expect(base_project).to be_equivalent_to_project(theirs_project)
503
+ end
504
+
505
+ it "adds values to existing build setting" do
506
+ base_project.targets[0].build_configurations.each do |configuration|
507
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
508
+ "$(SRCROOT)/../Foo"
509
+ ]
510
+ end
511
+
512
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
513
+
514
+ theirs_project.targets[0].build_configurations.each do |configuration|
515
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
516
+ "$(SRCROOT)/../Foo",
517
+ "$(SRCROOT)/../Bar"
518
+ ]
519
+ end
520
+
521
+ changes_to_apply = get_diff(theirs_project, base_project)
522
+
523
+ described_class.apply_change_to_project(base_project, changes_to_apply)
524
+ base_project.save
525
+
526
+ expect(base_project).to be_equivalent_to_project(theirs_project)
527
+ end
528
+
529
+ it "removes build setting" do
530
+ base_project.targets[0].build_configurations.each do |configuration|
531
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = [
532
+ "$(SRCROOT)/../Foo",
533
+ "$(SRCROOT)/../Bar"
534
+ ]
535
+ end
536
+
537
+ base_project.save
538
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
539
+
540
+ theirs_project.targets[0].build_configurations.each do |configuration|
541
+ configuration.build_settings["HEADER_SEARCH_PATHS"] = nil
542
+ end
543
+
544
+ changes_to_apply = get_diff(theirs_project, base_project)
545
+
546
+ described_class.apply_change_to_project(base_project, changes_to_apply)
547
+ base_project.save
548
+
549
+ expect(base_project).to be_equivalent_to_project(theirs_project)
550
+ end
551
+
552
+ it "adds build phases" do
553
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
554
+
555
+ theirs_project.targets[0].new_shell_script_build_phase("bar")
556
+ theirs_project.targets[0].source_build_phase
557
+ theirs_project.targets[0].headers_build_phase
558
+ theirs_project.targets[0].frameworks_build_phase
559
+ theirs_project.targets[0].resources_build_phase
560
+ theirs_project.targets[0].new_copy_files_build_phase("baz")
561
+
562
+ changes_to_apply = get_diff(theirs_project, base_project)
563
+
564
+ described_class.apply_change_to_project(base_project, changes_to_apply)
565
+ base_project.save
566
+
567
+ expect(base_project).to be_equivalent_to_project(theirs_project)
568
+ end
569
+
570
+ it "removes build phase" do
571
+ base_project.targets[0].new_shell_script_build_phase("bar")
572
+
573
+ base_project.save
574
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
575
+
576
+ theirs_project.targets[0].shell_script_build_phases[0].remove_from_project
577
+
578
+ changes_to_apply = get_diff(theirs_project, base_project)
579
+
580
+ described_class.apply_change_to_project(base_project, changes_to_apply)
581
+ base_project.save
582
+
583
+ expect(base_project).to be_equivalent_to_project(theirs_project)
584
+ end
585
+
586
+ it "ignores localizations in build settings added to existing localization files" do
587
+ variant_group = base_project.main_group.new_variant_group("foo.strings")
588
+ file = variant_group.new_reference("Base")
589
+ file.last_known_file_type = "text.plist.strings"
590
+ target.resources_build_phase.add_file_reference(variant_group)
591
+
592
+ base_project.save
593
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
594
+
595
+ theirs_variant_group = theirs_project.main_group.find_subpath("foo.strings")
596
+ theirs_variant_group.new_reference("en")
597
+
598
+ changes_to_apply = get_diff(theirs_project, base_project)
599
+
600
+ described_class.apply_change_to_project(base_project, changes_to_apply)
601
+ base_project.save
602
+
603
+ expect(base_project).to be_equivalent_to_project(theirs_project)
604
+ end
605
+
606
+ it "adds target dependency" do
607
+ base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
608
+
609
+ base_project.save
610
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
611
+
612
+ theirs_project.targets[1].add_dependency(theirs_project.targets[0])
613
+
614
+ changes_to_apply = get_diff(theirs_project, base_project)
615
+
616
+ described_class.apply_change_to_project(base_project, changes_to_apply)
617
+ base_project.save
618
+
619
+ expect(base_project).to be_equivalent_to_project(theirs_project)
620
+ end
621
+
622
+ it "changes value of a string build setting" do
623
+ base_project.targets[0].build_configurations.each do |configuration|
624
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "foo"
625
+ end
626
+
627
+ base_project.save
628
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
629
+
630
+ theirs_project.targets[0].build_configurations.each do |configuration|
631
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "bar"
632
+ end
633
+
634
+ changes_to_apply = get_diff(theirs_project, base_project)
635
+
636
+ described_class.apply_change_to_project(base_project, changes_to_apply)
637
+ base_project.save
638
+
639
+ expect(base_project).to be_equivalent_to_project(theirs_project)
640
+ end
641
+
642
+ it "adds build settings to new target" do
643
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
644
+
645
+ theirs_project.new_target("com.apple.product-type.library.static", "bar", :ios)
646
+
647
+ theirs_project.targets[1].build_configurations.each do |configuration|
648
+ configuration.build_settings["GCC_PREFIX_HEADER"] = "baz"
649
+ end
650
+
651
+ changes_to_apply = get_diff(theirs_project, base_project)
652
+
653
+ described_class.apply_change_to_project(base_project, changes_to_apply)
654
+ base_project.save
655
+
656
+ expect(base_project).to be_equivalent_to_project(theirs_project)
657
+ end
658
+
659
+ it "adds base configuration reference to new target" do
660
+ base_project.main_group.new_reference("baz")
661
+
662
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
663
+
664
+ configuration_reference = theirs_project.main_group.find_subpath("baz")
665
+ theirs_project.targets[0].build_configurations.each do |configuration|
666
+ configuration.base_configuration_reference = configuration_reference
667
+ end
668
+
669
+ changes_to_apply = get_diff(theirs_project, base_project)
670
+
671
+ described_class.apply_change_to_project(base_project, changes_to_apply)
672
+ base_project.save
673
+
674
+ expect(base_project).to be_equivalent_to_project(theirs_project)
675
+ end
676
+ end
677
+
678
+ it "adds known regions" do
679
+ base_project.save
680
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
681
+
682
+ theirs_project.root_object.known_regions += ["en"]
683
+
684
+ changes_to_apply = get_diff(theirs_project, base_project)
685
+
686
+ described_class.apply_change_to_project(base_project, changes_to_apply)
687
+ base_project.save
688
+
689
+ expect(base_project).to be_equivalent_to_project(theirs_project)
690
+ end
691
+
692
+ it "removes known regions" do
693
+ base_project.root_object.known_regions += ["en"]
694
+
695
+ base_project.save
696
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
697
+
698
+ theirs_project.root_object.known_regions = []
699
+
700
+ changes_to_apply = get_diff(theirs_project, base_project)
701
+
702
+ described_class.apply_change_to_project(base_project, changes_to_apply)
703
+ base_project.save
704
+
705
+ expect(base_project).to be_equivalent_to_project(theirs_project)
706
+ end
707
+
708
+ it "adds attribute target changes even if target attributes don't exist" do
709
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
710
+
711
+ theirs_project.root_object.attributes["TargetAttributes"] =
712
+ {"foo" => {"LastSwiftMigration" => "1140"}}
713
+
714
+ changes_to_apply = get_diff(theirs_project, base_project)
715
+
716
+ described_class.apply_change_to_project(base_project, changes_to_apply)
717
+ base_project.save
718
+
719
+ expect(base_project).to be_equivalent_to_project(theirs_project)
720
+ end
721
+
722
+ it "adds attribute target changes of new target" do
723
+ base_project.root_object.attributes["TargetAttributes"] = {}
724
+ base_project.save
725
+
726
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
727
+
728
+ theirs_project.root_object.attributes["TargetAttributes"] =
729
+ {"foo" => {"LastSwiftMigration" => "1140"}}
730
+
731
+ changes_to_apply = get_diff(theirs_project, base_project)
732
+
733
+ described_class.apply_change_to_project(base_project, changes_to_apply)
734
+ base_project.save
735
+
736
+ expect(base_project).to be_equivalent_to_project(theirs_project)
737
+ end
738
+
739
+ it "adds attribute target changes of existing target" do
740
+ base_project.root_object.attributes["TargetAttributes"] = {"foo" => {}}
741
+ base_project.save
742
+
743
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
744
+
745
+ theirs_project.root_object.attributes["TargetAttributes"] =
746
+ {"foo" => {"LastSwiftMigration" => "1140"}}
747
+
748
+ changes_to_apply = get_diff(theirs_project, base_project)
749
+
750
+ described_class.apply_change_to_project(base_project, changes_to_apply)
751
+ base_project.save
752
+
753
+ expect(base_project).to be_equivalent_to_project(theirs_project)
754
+ end
755
+
756
+ it "removes attribute target changes" do
757
+ base_project.root_object.attributes["TargetAttributes"] =
758
+ {"foo" => {"LastSwiftMigration" => "1140"}}
759
+ base_project.save
760
+
761
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
762
+
763
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = {}
764
+
765
+ changes_to_apply = get_diff(theirs_project, base_project)
766
+
767
+ described_class.apply_change_to_project(base_project, changes_to_apply)
768
+ base_project.save
769
+
770
+ expect(base_project).to be_equivalent_to_project(theirs_project)
771
+ end
772
+
773
+ it "identifies subproject added in separate times" do
774
+ framework_filename = "baz"
775
+
776
+ subproject = new_subproject("subproj", framework_filename)
777
+
778
+ add_existing_subproject_to_project(base_project, subproject, framework_filename)
779
+ base_project.save
780
+
781
+ theirs_project_path = make_temp_directory("theirs", ".xcodeproj")
782
+ theirs_project = Xcodeproj::Project.new(theirs_project_path)
783
+ add_existing_subproject_to_project(theirs_project, subproject, framework_filename)
784
+ theirs_project.save
785
+ ours_project = create_copy_of_project(theirs_project_path, "other_theirs")
786
+
787
+ subproject.new_target("com.apple.product-type.library.static", "bari", :ios)
788
+ ours_project.root_object.project_references[0][:product_group] <<
789
+ create_reference_proxy_from_product_reference(theirs_project,
790
+ theirs_project.root_object.project_references[0][:project_ref],
791
+ subproject.products_group.files[-1])
792
+
793
+ changes_to_apply = get_diff(ours_project, theirs_project)
794
+
795
+ described_class.apply_change_to_project(base_project, changes_to_apply)
796
+ base_project.save
797
+
798
+ expect(base_project).to be_equivalent_to_project(ours_project, ignore_keys: ["containerPortal"])
799
+ end
800
+
801
+ it "adds localization files" do
802
+ base_project_path = make_temp_directory("base", ".xcodeproj")
803
+ base_project = Xcodeproj::Project.new(base_project_path)
804
+ base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
805
+
806
+ base_project.save
807
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
808
+
809
+ variant_group = theirs_project.main_group.new_variant_group("foo.strings")
810
+ variant_group.new_reference("Base").last_known_file_type = "text.plist.strings"
811
+ theirs_project.targets[0].resources_build_phase.add_file_reference(variant_group)
812
+
813
+ changes_to_apply = get_diff(theirs_project, base_project)
814
+
815
+ described_class.apply_change_to_project(base_project, changes_to_apply)
816
+
817
+ expect(base_project).to be_equivalent_to_project(theirs_project)
818
+ end
819
+
820
+ def create_copy_of_project(project_path, new_project_prefix)
821
+ copied_project_path = make_temp_directory(new_project_prefix, ".xcodeproj")
822
+ FileUtils.cp(File.join(project_path, "project.pbxproj"), copied_project_path)
823
+ Xcodeproj::Project.open(copied_project_path)
824
+ end
825
+
826
+ def get_diff(first_project, second_project)
827
+ Xcodeproj::Differ.project_diff(first_project, second_project, :added, :removed)
828
+ end
829
+
830
+ def add_new_subproject_to_project(project, subproject_name, subproject_product_name)
831
+ subproject = new_subproject(subproject_name, subproject_product_name)
832
+ add_existing_subproject_to_project(project, subproject, subproject_product_name)
833
+ subproject
834
+ end
835
+
836
+ def new_subproject(subproject_name, subproject_product_name)
837
+ subproject_path = make_temp_directory(subproject_name, ".xcodeproj")
838
+ subproject = Xcodeproj::Project.new(subproject_path)
839
+ subproject.new_target("com.apple.product-type.library.static", subproject_product_name, :ios)
840
+ subproject.save
841
+
842
+ subproject
843
+ end
844
+
845
+ def add_existing_subproject_to_project(project, subproject, subproject_product_name)
846
+ subproject_reference = project.new_file(subproject.path, :built_products)
847
+
848
+ # Workaround for a bug in xcodeproj: https://github.com/CocoaPods/Xcodeproj/issues/678
849
+ project.main_group.find_subpath("Products").children.find do |file_reference|
850
+ # The name of the added file reference is equivalent to the name of the product.
851
+ file_reference.path == subproject_product_name
852
+ end.remove_from_project
853
+
854
+ project.root_object.project_references[0][:product_group] =
855
+ project.new(Xcodeproj::Project::PBXGroup)
856
+ project.root_object.project_references[0][:product_group].name = "Products"
857
+ project.root_object.project_references[0][:product_group] <<
858
+ create_reference_proxy_from_product_reference(project, subproject_reference,
859
+ subproject.products_group.files[0])
860
+ end
861
+
862
+ def create_reference_proxy_from_product_reference(project, subproject_reference,
863
+ product_reference)
864
+ container_proxy = project.new(Xcodeproj::Project::PBXContainerItemProxy)
865
+ container_proxy.container_portal = subproject_reference.uuid
866
+ container_proxy.proxy_type = Xcodeproj::Constants::PROXY_TYPES[:reference]
867
+ container_proxy.remote_global_id_string = product_reference.uuid
868
+ container_proxy.remote_info = subproject_reference.name
869
+
870
+ reference_proxy = project.new(Xcodeproj::Project::PBXReferenceProxy)
871
+ extension = File.extname(product_reference.path)[1..-1]
872
+ reference_proxy.file_type = Xcodeproj::Constants::FILE_TYPES_BY_EXTENSION[extension]
873
+ reference_proxy.path = product_reference.path
874
+ reference_proxy.remote_ref = container_proxy
875
+ reference_proxy.source_tree = 'BUILT_PRODUCTS_DIR'
876
+
877
+ reference_proxy
878
+ end
879
+
880
+ def make_temp_directory(directory_prefix, directory_extension)
881
+ directory_path = Dir.mktmpdir([directory_prefix, directory_extension])
882
+ temporary_directories_paths << directory_path
883
+ directory_path
884
+ end
885
+ end