kintsugi 0.2.0 → 0.4.1

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 1d58044d1f7c23e62f290c33defa4d9b3e95c0012b62754997a7c628d96e4358
4
- data.tar.gz: 6d63f1e0cf56c131fe7222c9d316f4f152310077fa2a9f4e386fba54ab82a340
3
+ metadata.gz: 7efc466f1583ea53deda2b4e77f030a1072ac4e93f8d8687f5eca22e2ea1d60c
4
+ data.tar.gz: 0c9dd29ad25b68fbb8c7d2b3b7ce4113e95a341234edaa3d3e11277dd691fc04
5
5
  SHA512:
6
- metadata.gz: 1aa18d3fd3456e1aa44f6497bc3e2f4e4877836e8722887050725012591e43b0086efa490df8eab8f1b63800f900897365a81c87935fb68341b1727e4c432e99
7
- data.tar.gz: f8aa152a98e35b9620a3c855eb010781a7fb65e843c6323ea4d14fb27ba414416c2e585273f797e0d8698dfae67d9f7a963e626cc2a2f40ca0d6cce0707cfc0d
6
+ metadata.gz: ee93b9b5f72d5ab35fb37a6fcc5464815e9ea13a9a985296d9132bd04be02edc9f542564b6cf5f495bc1a680b9b06a317bed0c709347ca641f5b31bb3145796e
7
+ data.tar.gz: c89520181be68ea61548b989a957e3d7fea7cef637b5a5cfa962ca3634b1db8d0e85906f947ea8868fa3a7ec5feed558d0ea156a77887f59d1123e4865bc6062
data/.rubocop.yml CHANGED
@@ -6,6 +6,9 @@ RSpec/ExampleLength:
6
6
  RSpec/DescribeClass:
7
7
  Enabled: false
8
8
 
9
+ RSpec/MultipleExpectations:
10
+ Enabled: false
11
+
9
12
  AllCops:
10
13
  DisplayCopNames: true
11
14
  TargetRubyVersion: 2.5
data/Gemfile CHANGED
@@ -2,5 +2,3 @@
2
2
 
3
3
  source "https://rubygems.org"
4
4
  gemspec
5
- gem "ruby-debug-ide", "0.7.2"
6
- gem "debase", "0.2.5.beta2"
data/README.md CHANGED
@@ -40,15 +40,22 @@ And see the magic happen! :sparkles:
40
40
 
41
41
  ### Git merge driver
42
42
 
43
- To use Kintsugi as a Git merge driver, follow these steps:
43
+ You can setup Kintsugi to automatically resolve conflicts that occur in `pbxproj` files when such conflicts occur.
44
44
 
45
- - Add it as driver to Git config file by running the following:
45
+ #### Automatic install
46
+
47
+ Run `kintsugi install-driver`. This will install Kintsugi as a merge driver globally. Note that Kintsugi needs to be in your `PATH`.
48
+
49
+ ❗ Do not install with bundler because the installation might succeed even if Kintsugi is not in `PATH`.
50
+
51
+ #### Manual install
52
+
53
+ - Add Kintsugi as driver to Git config file by running the following:
46
54
  ```sh
47
55
  git config merge.kintsugi.name "Kintsugi driver" # Or any other name you prefer
48
- git config merge.kintsugi.driver "kintsugi driver %O %A %B %P"
56
+ git config merge.kintsugi.driver "<path_to_kintsugi> driver %O %A %B %P"
49
57
  ```
50
58
 
51
- `kintsugi` should be in your `PATH`.
52
59
  Run `git config` with `--global` to add this to the global config file.
53
60
 
54
61
  - Add the following line to the `.gitattributes` file at the root of the repository:
@@ -65,7 +72,11 @@ See our [Contribution guidelines](./CONTRIBUTING.md).
65
72
 
66
73
  ## Alternatives
67
74
 
68
- - [XcodeGen](https://github.com/yonaskolb/XcodeGen): You can commit this JSON file into Git instead of the `.pbxproj` file. Then resolving conflicts is much easier.
75
+ All of the alternatives below allow you to generate your Xcode projects based on a spec or manifest. You commit these files to git, and can even remove the `.xcodeproj` files from git.
76
+
77
+ - [XcodeGen](https://github.com/yonaskolb/XcodeGen)
78
+ - [Tuist](https://github.com/tuist)
79
+ - [Xcake](https://github.com/igor-makarov/xcake)
69
80
 
70
81
  ## Copyright
71
82
 
data/bin/kintsugi CHANGED
@@ -6,6 +6,7 @@
6
6
 
7
7
  require "kintsugi"
8
8
  require_relative "../lib/kintsugi/cli"
9
+ require_relative "../lib/kintsugi/error"
9
10
 
10
11
  def parse_options!(command, argv)
11
12
  options = {}
@@ -28,4 +29,11 @@ command =
28
29
  end
29
30
 
30
31
  options = parse_options!(command, ARGV)
31
- command.action.call(options, ARGV, command.option_parser)
32
+
33
+ begin
34
+ command.action.call(options, ARGV)
35
+ rescue ArgumentError => e
36
+ puts "#{e.class}: #{e}"
37
+ rescue Kintsugi::MergeError => e
38
+ puts e
39
+ end
data/kintsugi.gemspec CHANGED
@@ -22,7 +22,7 @@ Gem::Specification.new do |spec|
22
22
  spec.test_files = spec.files.grep(%r{^(spec)/})
23
23
  spec.require_paths = ["lib"]
24
24
 
25
- spec.add_dependency "xcodeproj", "1.19.0"
25
+ spec.add_dependency "xcodeproj", ">= 1.19.0", "<= 1.21.0"
26
26
 
27
27
  spec.add_development_dependency "rake", "~> 13.0"
28
28
  spec.add_development_dependency "rspec", "~> 3.9"
@@ -167,7 +167,7 @@ module Kintsugi
167
167
  new_value = nil
168
168
 
169
169
  if change.key?(:removed)
170
- new_value = apply_removal_to_simple_attribute(old_value, change[:removed])
170
+ new_value = apply_removal_to_simple_attribute(old_value, change[:removed], change[:added])
171
171
  end
172
172
 
173
173
  if change.key?(:added)
@@ -183,30 +183,31 @@ module Kintsugi
183
183
  new_value
184
184
  end
185
185
 
186
- def apply_removal_to_simple_attribute(old_value, change)
187
- case change
186
+ def apply_removal_to_simple_attribute(old_value, removed_change, added_change)
187
+ case removed_change
188
188
  when Array
189
- (old_value || []) - change
189
+ (old_value || []) - removed_change
190
190
  when Hash
191
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}. This is considered a conflict that should be resolved manually."
192
+ if value != removed_change[key] && added_change[key] != value
193
+ raise MergeError, "Trying to remove value '#{removed_change[key]}' of hash with key " \
194
+ "'#{key}' but it changed to #{value}. This is considered a conflict that should be " \
195
+ "resolved manually."
195
196
  end
196
197
 
197
- change.key?(key)
198
+ removed_change.key?(key)
198
199
  end
199
200
  when String
200
- if old_value != change && !old_value.nil?
201
- raise "Trying to remove value #{change}, but the existing value is #{old_value}. This " \
202
- "is considered a conflict that should be resolved manually."
201
+ if old_value != removed_change && !old_value.nil? && added_change != old_value
202
+ raise MergeError, "Trying to remove value '#{removed_change}', but the existing value " \
203
+ "is '#{old_value}'. This is considered a conflict that should be resolved manually."
203
204
  end
204
205
 
205
206
  nil
206
207
  when nil
207
208
  nil
208
209
  else
209
- raise "Unsupported change #{change} of type #{change.class}"
210
+ raise MergeError, "Unsupported change #{removed_change} of type #{removed_change.class}"
210
211
  end
211
212
  end
212
213
 
@@ -219,7 +220,8 @@ module Kintsugi
219
220
  new_value = old_value.merge(change)
220
221
 
221
222
  unless (old_value.to_a - new_value.to_a).empty?
222
- raise "New hash #{change} contains values that conflict with old hash #{old_value}"
223
+ raise MergeError, "New hash #{change} contains values that conflict with old hash " \
224
+ "#{old_value}"
223
225
  end
224
226
 
225
227
  new_value
@@ -228,14 +230,15 @@ module Kintsugi
228
230
  when nil
229
231
  nil
230
232
  else
231
- raise "Unsupported change #{change} of type #{change.class}"
233
+ raise MergeError, "Unsupported change #{change} of type #{change.class}"
232
234
  end
233
235
  end
234
236
 
235
237
  def remove_component(component, change)
236
238
  if component.to_tree_hash != change
237
- raise "Trying to remove an object that changed since then. This is considered a conflict " \
238
- "that should be resolved manually. Name of the object is: '#{component.display_name}'"
239
+ raise MergeError, "Trying to remove an object that changed since then. This is " \
240
+ "considered a conflict that should be resolved manually. Name of the object is: " \
241
+ "'#{component.display_name}'"
239
242
  end
240
243
 
241
244
  if change["isa"] == "PBXFileReference"
@@ -266,6 +269,8 @@ module Kintsugi
266
269
  case change["isa"]
267
270
  when "PBXNativeTarget"
268
271
  add_target(component, change)
272
+ when "PBXAggregateTarget"
273
+ add_aggregate_target(component, change)
269
274
  when "PBXFileReference"
270
275
  add_file_reference(component, change)
271
276
  when "PBXGroup"
@@ -299,21 +304,36 @@ module Kintsugi
299
304
  when "PBXReferenceProxy"
300
305
  add_reference_proxy(component, change)
301
306
  else
302
- raise "Trying to add unsupported component type #{change["isa"]}. Full component change " \
303
- "is: #{change}"
307
+ raise MergeError, "Trying to add unsupported component type #{change["isa"]}. Full " \
308
+ "component change is: #{change}"
304
309
  end
305
310
  end
306
311
 
307
312
  def add_reference_proxy(containing_component, change)
308
313
  case containing_component
309
314
  when Xcodeproj::Project::PBXBuildFile
310
- containing_component.file_ref = find_file(containing_component.project, change)
315
+ # If there are two file references that refer to the same file, one with a build file and
316
+ # the other one without, this method will prefer to take the one without the build file.
317
+ # This assumes that it's preferred to have a file reference with build file than a file
318
+ # reference without/with two build files.
319
+ filter_references_without_build_files = lambda do |reference|
320
+ reference.referrers.find do |referrer|
321
+ referrer.is_a?(Xcodeproj::Project::PBXBuildFile)
322
+ end.nil?
323
+ end
324
+ file_reference =
325
+ find_reference_proxy(containing_component.project, change["remoteRef"],
326
+ reference_filter: filter_references_without_build_files)
327
+ if file_reference.nil?
328
+ file_reference = find_reference_proxy(containing_component.project, change["remoteRef"])
329
+ end
330
+ containing_component.file_ref = file_reference
311
331
  when Xcodeproj::Project::PBXGroup
312
332
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
313
333
  containing_component << reference_proxy
314
334
  add_attributes_to_component(reference_proxy, change)
315
335
  else
316
- raise "Trying to add reference proxy to an unsupported component type " \
336
+ raise MergeError, "Trying to add reference proxy to an unsupported component type " \
317
337
  "#{containing_component.isa}. Change is: #{change}"
318
338
  end
319
339
  end
@@ -328,7 +348,7 @@ module Kintsugi
328
348
  containing_component.children << variant_group
329
349
  add_attributes_to_component(variant_group, change)
330
350
  else
331
- raise "Trying to add variant group to an unsupported component type " \
351
+ raise MergeError, "Trying to add variant group to an unsupported component type " \
332
352
  "#{containing_component.isa}. Change is: #{change}"
333
353
  end
334
354
  end
@@ -424,7 +444,7 @@ module Kintsugi
424
444
  when "PBXReferenceProxy"
425
445
  component.remote_ref = container_proxy
426
446
  else
427
- raise "Trying to add container item proxy to an unsupported component type " \
447
+ raise MergeError, "Trying to add container item proxy to an unsupported component type " \
428
448
  "#{containing_component.isa}. Change is: #{change}"
429
449
  end
430
450
  add_attributes_to_component(container_proxy, change, ignore_keys: ["containerPortal"])
@@ -459,7 +479,14 @@ module Kintsugi
459
479
  end
460
480
 
461
481
  def add_subproject_reference(root_object, project_reference_change)
462
- subproject_reference = find_file(root_object.project, project_reference_change["ProjectRef"])
482
+ filter_subproject_without_project_references = lambda do |file_reference|
483
+ root_object.project_references.find do |project_reference|
484
+ project_reference.project_ref.uuid == file_reference.uuid
485
+ end.nil?
486
+ end
487
+ subproject_reference =
488
+ find_file(root_object.project, project_reference_change["ProjectRef"],
489
+ file_filter: filter_subproject_without_project_references)
463
490
 
464
491
  attribute =
465
492
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
@@ -489,6 +516,12 @@ module Kintsugi
489
516
  add_attributes_to_component(target, change)
490
517
  end
491
518
 
519
+ def add_aggregate_target(root_object, change)
520
+ target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
521
+ root_object.project.targets << target
522
+ add_attributes_to_component(target, change)
523
+ end
524
+
492
525
  def add_file_reference(containing_component, change)
493
526
  # base configuration reference and product reference always reference a file that exists
494
527
  # inside a group, therefore in these cases the file is searched for.
@@ -510,7 +543,7 @@ module Kintsugi
510
543
  file_reference.include_in_index = nil
511
544
  add_attributes_to_component(file_reference, change)
512
545
  else
513
- raise "Trying to add file reference to an unsupported component type " \
546
+ raise MergeError, "Trying to add file reference to an unsupported component type " \
514
547
  "#{containing_component.isa}. Change is: #{change}"
515
548
  end
516
549
  end
@@ -525,8 +558,8 @@ module Kintsugi
525
558
  new_group = containing_component.project.new(Xcodeproj::Project::PBXGroup)
526
559
  containing_component.children << new_group
527
560
  else
528
- raise "Trying to add group to an unsupported component type #{containing_component.isa}. " \
529
- "Change is: #{change}"
561
+ raise MergeError, "Trying to add group to an unsupported component type " \
562
+ "#{containing_component.isa}. Change is: #{change}"
530
563
  end
531
564
 
532
565
  add_attributes_to_component(new_group, change)
@@ -550,31 +583,35 @@ module Kintsugi
550
583
  add_child_to_component(component, added_attribute_element)
551
584
  end
552
585
  else
553
- raise "Trying to add attribute of unsupported type '#{change_value.class}' to " \
554
- "object #{component}. Attribute name is '#{change_name}'"
586
+ raise MergeError, "Trying to add attribute of unsupported type '#{change_value.class}' " \
587
+ "to object #{component}. Attribute name is '#{change_name}'"
555
588
  end
556
589
  end
557
590
  end
558
591
 
559
- def find_file(project, file_reference_change)
560
- case file_reference_change["isa"]
561
- when "PBXFileReference"
562
- project.files.find do |file_reference|
563
- next file_reference.path == file_reference_change["path"]
564
- end
565
- when "PBXReferenceProxy"
566
- find_reference_proxy(project, file_reference_change["remoteRef"])
567
- else
568
- raise "Unsupported file reference change of type #{file_reference["isa"]}."
592
+ def find_file(project, file_reference_change, file_filter: ->(_) { true })
593
+ file_references = project.files.select do |file_reference|
594
+ file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
595
+ end
596
+ if file_references.length > 1
597
+ puts "Debug: Found more than one matching file with path " \
598
+ "'#{file_reference_change["path"]}'. Using the first one."
599
+ elsif file_references.empty?
600
+ puts "Debug: No file reference found for file with path " \
601
+ "'#{file_reference_change["path"]}'."
602
+ return
569
603
  end
604
+
605
+ file_references.first
570
606
  end
571
607
 
572
- def find_reference_proxy(project, container_item_proxy_change)
608
+ def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
573
609
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
574
610
  project_ref_and_products[:product_group].children.find do |product|
575
611
  product.remote_ref.remote_global_id_string ==
576
612
  container_item_proxy_change["remoteGlobalIDString"] &&
577
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
613
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
614
+ reference_filter.call(product)
578
615
  end
579
616
  end.compact
580
617
 
data/lib/kintsugi/cli.rb CHANGED
@@ -2,6 +2,7 @@
2
2
  # Created by Ben Yohay.
3
3
  # frozen_string_literal: true
4
4
 
5
+ require "fileutils"
5
6
  require "optparse"
6
7
 
7
8
  require_relative "version"
@@ -12,12 +13,14 @@ module Kintsugi
12
13
  # Subcommands of Kintsugi CLI.
13
14
  attr_reader :subcommands
14
15
 
15
- # Root command Kintsugi CLI.
16
+ # Root command of Kintsugi CLI.
16
17
  attr_reader :root_command
17
18
 
18
19
  def initialize
19
20
  @subcommands = {
20
- "driver" => create_driver_subcommand
21
+ "driver" => create_driver_subcommand,
22
+ "install-driver" => create_install_driver_subcommand,
23
+ "uninstall-driver" => create_uninstall_driver_subcommand
21
24
  }.freeze
22
25
  @root_command = create_root_command
23
26
  end
@@ -27,7 +30,7 @@ module Kintsugi
27
30
  Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
28
31
 
29
32
  def create_driver_subcommand
30
- driver_option_parser =
33
+ option_parser =
31
34
  OptionParser.new do |opts|
32
35
  opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
33
36
  "Uses Kintsugi as a Git merge driver. Parameters " \
@@ -40,7 +43,7 @@ module Kintsugi
40
43
  end
41
44
  end
42
45
 
43
- driver_action = lambda { |_, arguments, option_parser|
46
+ driver_action = lambda { |_, arguments|
44
47
  if arguments.count != 4
45
48
  puts "Incorrect number of arguments to 'driver' subcommand\n\n"
46
49
  puts option_parser
@@ -50,14 +53,112 @@ module Kintsugi
50
53
  }
51
54
 
52
55
  Command.new(
53
- option_parser: driver_option_parser,
56
+ option_parser: option_parser,
54
57
  action: driver_action,
55
58
  description: "3-way merge compatible with Git merge driver"
56
59
  )
57
60
  end
58
61
 
62
+ def create_install_driver_subcommand
63
+ option_parser =
64
+ OptionParser.new do |opts|
65
+ opts.banner = "Usage: kintsugi install-driver\n" \
66
+ "Installs Kintsugi as a Git merge driver globally. "
67
+
68
+ opts.on("-h", "--help", "Prints this help") do
69
+ puts opts
70
+ exit
71
+ end
72
+ end
73
+
74
+ action = lambda { |_, arguments|
75
+ if arguments.count != 0
76
+ puts "Incorrect number of arguments to 'install-driver' subcommand\n\n"
77
+ puts option_parser
78
+ exit(1)
79
+ end
80
+
81
+ if `which kintsugi`.chomp.empty?
82
+ puts "Can only install Kintsugi globally if Kintsugi is in your PATH"
83
+ exit(1)
84
+ end
85
+
86
+ install_kintsugi_driver_globally
87
+ puts "Done! 🪄"
88
+ }
89
+
90
+ Command.new(
91
+ option_parser: option_parser,
92
+ action: action,
93
+ description: "Installs Kintsugi as a Git merge driver globally"
94
+ )
95
+ end
96
+
97
+ def install_kintsugi_driver_globally
98
+ `git config --global merge.kintsugi.name "Kintsugi driver"`
99
+ `git config --global merge.kintsugi.driver "kintsugi driver %O %A %B %P"`
100
+
101
+ attributes_file_path = global_attributes_file_path
102
+ FileUtils.mkdir_p(File.dirname(attributes_file_path))
103
+
104
+ merge_using_kintsugi_line = "'*.pbxproj merge=kintsugi'"
105
+ `grep -sqxF #{merge_using_kintsugi_line} "#{attributes_file_path}" \
106
+ || echo #{merge_using_kintsugi_line} >> "#{attributes_file_path}"`
107
+ end
108
+
109
+ def global_attributes_file_path
110
+ # The logic to decide the path to the global attributes file is described at:
111
+ # https://git-scm.com/docs/gitattributes.
112
+ config_attributes_file_path = `git config --global core.attributesfile`
113
+ return config_attributes_file_path unless config_attributes_file_path.empty?
114
+
115
+ if ENV["XDG_CONFIG_HOME"].nil? || ENV["XDG_CONFIG_HOME"].empty?
116
+ File.join(ENV["HOME"], ".config/git/attributes")
117
+ else
118
+ File.join(ENV["XDG_CONFIG_HOME"], "git/attributes")
119
+ end
120
+ end
121
+
122
+ def create_uninstall_driver_subcommand
123
+ option_parser =
124
+ OptionParser.new do |opts|
125
+ opts.banner = "Usage: kintsugi uninstall-driver\n" \
126
+ "Uninstalls Kintsugi as a Git merge driver that was previously installed globally."
127
+
128
+ opts.on("-h", "--help", "Prints this help") do
129
+ puts opts
130
+ exit
131
+ end
132
+ end
133
+
134
+ action = lambda { |_, arguments|
135
+ if arguments.count != 0
136
+ puts "Incorrect number of arguments to 'uninstall-driver' subcommand\n\n"
137
+ puts option_parser
138
+ exit(1)
139
+ end
140
+
141
+ uninstall_kintsugi_driver_globally
142
+ puts "Done!"
143
+ }
144
+
145
+ Command.new(
146
+ option_parser: option_parser,
147
+ action: action,
148
+ description: "Uninstalls Kintsugi as a Git merge driver that was previously installed " \
149
+ "globally."
150
+ )
151
+ end
152
+
153
+ def uninstall_kintsugi_driver_globally
154
+ `git config --global --unset merge.kintsugi.name`
155
+ `git config --global --unset merge.kintsugi.driver`
156
+
157
+ `sed -i '' '/\*.pbxproj\ merge=kintsugi/d' "#{global_attributes_file_path}"`
158
+ end
159
+
59
160
  def create_root_command
60
- root_option_parser = OptionParser.new do |opts|
161
+ option_parser = OptionParser.new do |opts|
61
162
  opts.banner = "Kintsugi, version #{Version::STRING}\n" \
62
163
  "Copyright (c) 2021 Lightricks\n\n" \
63
164
  "Usage: kintsugi <pbxproj_filepath> [options]\n" \
@@ -83,7 +184,7 @@ module Kintsugi
83
184
  opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
84
185
  end
85
186
 
86
- root_action = lambda { |options, arguments, option_parser|
187
+ root_action = lambda { |options, arguments|
87
188
  if arguments.count != 1
88
189
  puts "Incorrect number of arguments\n\n"
89
190
  puts option_parser
@@ -96,7 +197,7 @@ module Kintsugi
96
197
  }
97
198
 
98
199
  Command.new(
99
- option_parser: root_option_parser,
200
+ option_parser: option_parser,
100
201
  action: root_action,
101
202
  description: nil
102
203
  )
@@ -0,0 +1,9 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ module Kintsugi
6
+ # Raised when an error occurred while Kintsugi tried to resolve conflicts.
7
+ class MergeError < RuntimeError
8
+ end
9
+ end
@@ -3,6 +3,6 @@
3
3
  module Kintsugi
4
4
  # This module holds the Kintsugi version information.
5
5
  module Version
6
- STRING = "0.2.0"
6
+ STRING = "0.4.1"
7
7
  end
8
8
  end
data/lib/kintsugi.rb CHANGED
@@ -2,12 +2,14 @@
2
2
  # Created by Ben Yohay.
3
3
  # frozen_string_literal: true
4
4
 
5
+ require "json"
5
6
  require "tmpdir"
6
7
  require "tempfile"
7
8
  require "xcodeproj"
8
9
 
9
10
  require_relative "kintsugi/xcodeproj_extensions"
10
11
  require_relative "kintsugi/apply_change_to_project"
12
+ require_relative "kintsugi/error"
11
13
 
12
14
  module Kintsugi
13
15
  class << self
@@ -16,15 +18,15 @@ module Kintsugi
16
18
  # @param [String] project_file_path
17
19
  # Project to which to apply the changes.
18
20
  #
19
- # @param [String] output_changes_path
21
+ # @param [String] changes_output_path
20
22
  # Path to where the changes to apply to the project are written in JSON format.
21
23
  #
22
24
  # @raise [ArgumentError]
23
- # If the file extension is not `pbxproj` or the file doesn't exist
25
+ # If the file extension is not `pbxproj`, or the file doesn't exist, or if no rebase,
26
+ # cherry-pick, or merge is in progress
24
27
  #
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
+ # @raise [MergeError]
29
+ # If there was an error applying the change to the project.
28
30
  #
29
31
  # @return [void]
30
32
  def resolve_conflicts(project_file_path, changes_output_path)
@@ -58,7 +60,7 @@ module Kintsugi
58
60
  # @param [String] original_project_path
59
61
  # Path to the original path of the file.
60
62
  #
61
- # @raise [RuntimeError]
63
+ # @raise [MergeError]
62
64
  # If there was an error applying the change to the project.
63
65
  #
64
66
  # @return [void]
@@ -103,8 +105,8 @@ module Kintsugi
103
105
 
104
106
  Dir.chdir(File.dirname(project_file_path)) do
105
107
  unless file_has_base_ours_and_theirs_versions?(project_file_path)
106
- raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, or a 3-way " \
107
- "merge is not possible."
108
+ raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
109
+ "or a 3-way merge is not possible."
108
110
  end
109
111
  end
110
112
  end
@@ -38,6 +38,18 @@ describe Kintsugi, :apply_change_to_project do
38
38
  expect(base_project).to be_equivalent_to_project(theirs_project)
39
39
  end
40
40
 
41
+ it "adds new aggregate target" do
42
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
43
+ theirs_project.new_aggregate_target("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)
51
+ end
52
+
41
53
  it "adds new subproject" do
42
54
  theirs_project = create_copy_of_project(base_project.path, "theirs")
43
55
  add_new_subproject_to_project(theirs_project, "foo", "foo")
@@ -50,6 +62,26 @@ describe Kintsugi, :apply_change_to_project do
50
62
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
51
63
  end
52
64
 
65
+ it "adds subproject that already exists" do
66
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
67
+
68
+ subproject = add_new_subproject_to_project(theirs_project, "foo", "foo")
69
+ theirs_project.save
70
+
71
+ ours_project = create_copy_of_project(base_project.path, "ours")
72
+ add_existing_subproject_to_project(ours_project, subproject, "foo")
73
+
74
+ changes_to_apply = get_diff(theirs_project, base_project)
75
+
76
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
77
+ ours_project.save
78
+
79
+ expect(ours_project.root_object.project_references[0][:project_ref].uuid)
80
+ .not_to equal(ours_project.root_object.project_references[1][:project_ref].uuid)
81
+ expect(ours_project.root_object.project_references[0][:project_ref].proxy_containers).not_to be_empty
82
+ expect(ours_project.root_object.project_references[1][:project_ref].proxy_containers).not_to be_empty
83
+ end
84
+
53
85
  # Checks that the order the changes are applied in is correct.
54
86
  it "adds new subproject and reference to its framework" do
55
87
  theirs_project = create_copy_of_project(base_project.path, "theirs")
@@ -144,6 +176,23 @@ describe Kintsugi, :apply_change_to_project do
144
176
  expect(base_project).to be_equivalent_to_project(theirs_project)
145
177
  end
146
178
 
179
+ it "changes simple attribute of a file that has a build file" do
180
+ target = base_project.new_target("com.apple.product-type.library.static", "bar", :ios)
181
+ file_reference = base_project.main_group.find_file_by_path(filepath)
182
+ target.frameworks_build_phase.add_file_reference(file_reference)
183
+ base_project.save
184
+
185
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
186
+ file_reference = theirs_project.main_group.find_file_by_path(filepath)
187
+ file_reference.include_in_index = "4"
188
+
189
+ changes_to_apply = get_diff(theirs_project, base_project)
190
+
191
+ described_class.apply_change_to_project(base_project, changes_to_apply)
192
+
193
+ expect(base_project).to be_equivalent_to_project(theirs_project)
194
+ end
195
+
147
196
  it "removes build files of a removed file" do
148
197
  target = base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
149
198
  target.source_build_phase.add_file_reference(
@@ -417,6 +466,30 @@ describe Kintsugi, :apply_change_to_project do
417
466
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
418
467
  end
419
468
 
469
+ it "adds build file to a file reference that already exist" do
470
+ file_reference = base_project.main_group.new_reference("bar")
471
+ base_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
472
+
473
+ base_project.main_group.new_reference("bar")
474
+
475
+ base_project.save
476
+
477
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
478
+
479
+ theirs_file_reference = theirs_project.main_group.files.find do |file|
480
+ !file.referrers.find { |referrer| referrer.is_a?(Xcodeproj::Project::PBXBuildFile) } &&
481
+ file.display_name == "bar"
482
+ end
483
+ theirs_project.targets[0].frameworks_build_phase.add_file_reference(theirs_file_reference)
484
+
485
+ changes_to_apply = get_diff(theirs_project, base_project)
486
+
487
+ described_class.apply_change_to_project(base_project, changes_to_apply)
488
+ base_project.save
489
+
490
+ expect(base_project).to be_equivalent_to_project(theirs_project)
491
+ end
492
+
420
493
  it "adds file reference to build file" do
421
494
  file_reference = base_project.main_group.new_reference("bar")
422
495
 
@@ -789,7 +862,25 @@ describe Kintsugi, :apply_change_to_project do
789
862
  expect(ours_project).to be_equivalent_to_project(theirs_project)
790
863
  end
791
864
 
792
- it "identifies subproject added in separate times" do
865
+ it "doesn't throw if existing attribute target change is same as added change" do
866
+ base_project.root_object.attributes["TargetAttributes"] = {"foo" => "1140"}
867
+ base_project.save
868
+
869
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
870
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
871
+
872
+ ours_project = create_copy_of_project(base_project.path, "ours")
873
+ ours_project.root_object.attributes["TargetAttributes"]["foo"] = "1111"
874
+
875
+ changes_to_apply = get_diff(theirs_project, base_project)
876
+
877
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
878
+ ours_project.save
879
+
880
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
881
+ end
882
+
883
+ it "identifies subproject added at separate times when adding a product to the subproject" do
793
884
  framework_filename = "baz"
794
885
 
795
886
  subproject = new_subproject("subproj", framework_filename)
@@ -870,10 +961,10 @@ describe Kintsugi, :apply_change_to_project do
870
961
  file_reference.path == subproject_product_name
871
962
  end.remove_from_project
872
963
 
873
- project.root_object.project_references[0][:product_group] =
964
+ project.root_object.project_references[-1][:product_group] =
874
965
  project.new(Xcodeproj::Project::PBXGroup)
875
- project.root_object.project_references[0][:product_group].name = "Products"
876
- project.root_object.project_references[0][:product_group] <<
966
+ project.root_object.project_references[-1][:product_group].name = "Products"
967
+ project.root_object.project_references[-1][:product_group] <<
877
968
  create_reference_proxy_from_product_reference(project, subproject_reference,
878
969
  subproject.products_group.files[0])
879
970
  end
metadata CHANGED
@@ -1,29 +1,35 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: kintsugi
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.2.0
4
+ version: 0.4.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ben Yohay
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2021-07-20 00:00:00.000000000 Z
11
+ date: 2022-01-02 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: xcodeproj
15
15
  requirement: !ruby/object:Gem::Requirement
16
16
  requirements:
17
- - - '='
17
+ - - ">="
18
18
  - !ruby/object:Gem::Version
19
19
  version: 1.19.0
20
+ - - "<="
21
+ - !ruby/object:Gem::Version
22
+ version: 1.21.0
20
23
  type: :runtime
21
24
  prerelease: false
22
25
  version_requirements: !ruby/object:Gem::Requirement
23
26
  requirements:
24
- - - '='
27
+ - - ">="
25
28
  - !ruby/object:Gem::Version
26
29
  version: 1.19.0
30
+ - - "<="
31
+ - !ruby/object:Gem::Version
32
+ version: 1.21.0
27
33
  - !ruby/object:Gem::Dependency
28
34
  name: rake
29
35
  requirement: !ruby/object:Gem::Requirement
@@ -117,6 +123,7 @@ files:
117
123
  - lib/kintsugi.rb
118
124
  - lib/kintsugi/apply_change_to_project.rb
119
125
  - lib/kintsugi/cli.rb
126
+ - lib/kintsugi/error.rb
120
127
  - lib/kintsugi/utils.rb
121
128
  - lib/kintsugi/version.rb
122
129
  - lib/kintsugi/xcodeproj_extensions.rb
@@ -143,7 +150,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
143
150
  - !ruby/object:Gem::Version
144
151
  version: '0'
145
152
  requirements: []
146
- rubygems_version: 3.1.2
153
+ rubyforge_project:
154
+ rubygems_version: 2.7.6.3
147
155
  signing_key:
148
156
  specification_version: 4
149
157
  summary: pbxproj files git conflicts solver