kintsugi 0.1.0 → 0.3.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: 3047191981783a65015d6646c15e0c4aa5cba3b3a6f32b6328163a4e8ca7ff37
4
- data.tar.gz: 0653aa77f904b249d285feade1d8b5d34a2fbd4ee25bd9431cb32be6462a938e
3
+ metadata.gz: cdc0655ad443f7511adec336aef55e40ed0bb5458cf5fd9ae7747294bf40881d
4
+ data.tar.gz: 214a8426b955d0c36b2ff51f55b2016ed69861ccc5d99fa2d463567cbc6872fc
5
5
  SHA512:
6
- metadata.gz: f15ab16432c5b3bf335a1b25cac66d2e7bc0b20ec29b3b374b6d88010efc804b292750b703af3aed353a2c01666e116b11290ed32d48731ac3471f94a964c70c
7
- data.tar.gz: 54220422787ba0bf75c1cb1594ad3dfed6414e7d95df1193de3e2550953aee798396ed99204e433e10522577b51f2655b15e90b98219c7c608dca6379557580e
6
+ metadata.gz: 79d21fb3b3ddcb977a0b09bb5f80d19d6337574cb5b975a342d261bd689b53ff387a4ef2c7f0fb9a33d6698ce0e739b76824515ecf4b869f5858228635d99047
7
+ data.tar.gz: 4a902033e2f4236d82f3db8b1fa832ec78e8201b264a32a11599b3c0df3b048bb82e5abee8cb3d3fac7e74ae4d6ff37029ae9cd809fee5ebbe65723c997862e7
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/README.md CHANGED
@@ -34,10 +34,31 @@ gem 'kintsugi', require: false
34
34
 
35
35
  ## Usage
36
36
 
37
- When there's a `.pbxproj` file with Git conflicts, run `kintsugi <path_to_pbxproj_file>`.
37
+ When there's a `.pbxproj` file with Git conflicts, and a 3-way merge is possible, run `kintsugi <path_to_pbxproj_file>`.
38
38
 
39
39
  And see the magic happen! :sparkles:
40
40
 
41
+ ### Git merge driver
42
+
43
+ To use Kintsugi as a Git merge driver, follow these steps:
44
+
45
+ - Add it as driver to Git config file by running the following:
46
+ ```sh
47
+ 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"
49
+ ```
50
+
51
+ `kintsugi` should be in your `PATH`.
52
+ Run `git config` with `--global` to add this to the global config file.
53
+
54
+ - Add the following line to the `.gitattributes` file at the root of the repository:
55
+
56
+ `*.pbxproj merge=kintsugi`
57
+
58
+ This will instruct Git to use Kintsugi as a merge driver for `.pbxproj` files.
59
+
60
+ See the [official docs](https://git-scm.com/docs/gitattributes) if you want to set this globally.
61
+
41
62
  ## Contribution
42
63
 
43
64
  See our [Contribution guidelines](./CONTRIBUTING.md).
data/bin/kintsugi CHANGED
@@ -4,41 +4,28 @@
4
4
  # Copyright (c) 2020 Lightricks. All rights reserved.
5
5
  # Created by Ben Yohay.
6
6
 
7
- require "json"
8
- require "optparse"
9
-
10
7
  require "kintsugi"
8
+ require_relative "../lib/kintsugi/cli"
11
9
 
12
- def parse_options!(argv)
13
- options_parser = create_options_parser
14
-
10
+ def parse_options!(command, argv)
15
11
  options = {}
16
- options_parser.parse!(argv, into: options)
17
-
18
- if argv.length != 1
19
- puts "Incorrect number of arguments\n\n"
20
- puts options_parser
21
- exit(1)
22
- end
23
-
12
+ command.option_parser.parse!(argv, into: options)
24
13
  options
25
14
  end
26
15
 
27
- def create_options_parser
28
- OptionParser.new do |opts|
29
- opts.banner = "Usage: kintsugi [pbxproj_filepath] [options]"
30
- opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
31
- "written in JSON format. Used for debug purposes.")
32
-
33
- opts.on("-h", "--help", "Prints this help") do
34
- puts opts
35
- exit
36
- end
37
- end
16
+ def name_of_subcommand?(subcommands, argument)
17
+ subcommands.include?(argument)
38
18
  end
39
19
 
40
- options = parse_options!(ARGV)
41
- project_file_path = File.expand_path(ARGV[0])
42
- Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
20
+ first_argument = ARGV[0]
21
+ cli = Kintsugi::CLI.new
22
+ command =
23
+ if name_of_subcommand?(cli.subcommands, first_argument)
24
+ ARGV.shift
25
+ cli.subcommands[first_argument]
26
+ else
27
+ cli.root_command
28
+ end
43
29
 
44
- puts "Resolved conflicts successfully"
30
+ options = parse_options!(command, ARGV)
31
+ command.action.call(options, ARGV, command.option_parser)
data/kintsugi.gemspec CHANGED
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "lib/kintsugi/version"
4
+
3
5
  Gem::Specification.new do |spec|
4
6
  spec.name = "kintsugi"
5
- spec.version = "0.1.0"
7
+ spec.version = Kintsugi::Version::STRING
6
8
  spec.authors = ["Ben Yohay"]
7
9
  spec.email = ["ben@lightricks.com"]
8
10
  spec.required_ruby_version = ">= 2.5.0"
@@ -20,7 +22,7 @@ Gem::Specification.new do |spec|
20
22
  spec.test_files = spec.files.grep(%r{^(spec)/})
21
23
  spec.require_paths = ["lib"]
22
24
 
23
- spec.add_dependency "xcodeproj", "1.19.0"
25
+ spec.add_dependency "xcodeproj", ">= 1.19.0", "<= 1.21.0"
24
26
 
25
27
  spec.add_development_dependency "rake", "~> 13.0"
26
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,29 +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}."
192
+ if value != removed_change[key] && added_change[key] != value
193
+ raise "Trying to remove value '#{removed_change[key]}' of hash with key '#{key}' but " \
194
+ "it changed to #{value}. This is considered a conflict that should be resolved " \
195
+ "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
201
- raise "Value changed from #{old_value} to #{change}."
201
+ if old_value != removed_change && !old_value.nil? && added_change != old_value
202
+ raise "Trying to remove value '#{removed_change}', but the existing value is " \
203
+ "'#{old_value}'. This is considered a conflict that should be resolved manually."
202
204
  end
203
205
 
204
206
  nil
205
207
  when nil
206
208
  nil
207
209
  else
208
- raise "Unsupported change #{change} of type #{change.class}"
210
+ raise "Unsupported change #{removed_change} of type #{removed_change.class}"
209
211
  end
210
212
  end
211
213
 
@@ -265,6 +267,8 @@ module Kintsugi
265
267
  case change["isa"]
266
268
  when "PBXNativeTarget"
267
269
  add_target(component, change)
270
+ when "PBXAggregateTarget"
271
+ add_aggregate_target(component, change)
268
272
  when "PBXFileReference"
269
273
  add_file_reference(component, change)
270
274
  when "PBXGroup"
@@ -306,7 +310,22 @@ module Kintsugi
306
310
  def add_reference_proxy(containing_component, change)
307
311
  case containing_component
308
312
  when Xcodeproj::Project::PBXBuildFile
309
- containing_component.file_ref = find_file(containing_component.project, change)
313
+ # If there are two file references that refer to the same file, one with a build file and
314
+ # the other one without, this method will prefer to take the one without the build file.
315
+ # This assumes that it's preferred to have a file reference with build file than a file
316
+ # reference without/with two build files.
317
+ filter_references_without_build_files = lambda do |reference|
318
+ reference.referrers.find do |referrer|
319
+ referrer.is_a?(Xcodeproj::Project::PBXBuildFile)
320
+ end.nil?
321
+ end
322
+ file_reference =
323
+ find_reference_proxy(containing_component.project, change["remoteRef"],
324
+ reference_filter: filter_references_without_build_files)
325
+ if file_reference.nil?
326
+ file_reference = find_reference_proxy(containing_component.project, change["remoteRef"])
327
+ end
328
+ containing_component.file_ref = file_reference
310
329
  when Xcodeproj::Project::PBXGroup
311
330
  reference_proxy = containing_component.project.new(Xcodeproj::Project::PBXReferenceProxy)
312
331
  containing_component << reference_proxy
@@ -458,7 +477,14 @@ module Kintsugi
458
477
  end
459
478
 
460
479
  def add_subproject_reference(root_object, project_reference_change)
461
- subproject_reference = find_file(root_object.project, project_reference_change["ProjectRef"])
480
+ filter_subproject_without_project_references = lambda do |file_reference|
481
+ root_object.project_references.find do |project_reference|
482
+ project_reference.project_ref.uuid == file_reference.uuid
483
+ end.nil?
484
+ end
485
+ subproject_reference =
486
+ find_file(root_object.project, project_reference_change["ProjectRef"],
487
+ file_filter: filter_subproject_without_project_references)
462
488
 
463
489
  attribute =
464
490
  Xcodeproj::Project::PBXProject.references_by_keys_attributes
@@ -488,6 +514,12 @@ module Kintsugi
488
514
  add_attributes_to_component(target, change)
489
515
  end
490
516
 
517
+ def add_aggregate_target(root_object, change)
518
+ target = root_object.project.new(Xcodeproj::Project::PBXAggregateTarget)
519
+ root_object.project.targets << target
520
+ add_attributes_to_component(target, change)
521
+ end
522
+
491
523
  def add_file_reference(containing_component, change)
492
524
  # base configuration reference and product reference always reference a file that exists
493
525
  # inside a group, therefore in these cases the file is searched for.
@@ -555,25 +587,29 @@ module Kintsugi
555
587
  end
556
588
  end
557
589
 
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"]}."
590
+ def find_file(project, file_reference_change, file_filter: ->(_) { true })
591
+ file_references = project.files.select do |file_reference|
592
+ file_reference.path == file_reference_change["path"] && file_filter.call(file_reference)
593
+ end
594
+ if file_references.length > 1
595
+ puts "Debug: Found more than one matching file with path " \
596
+ "'#{file_reference_change["path"]}'. Using the first one."
597
+ elsif file_references.empty?
598
+ puts "Debug: No file reference found for file with path " \
599
+ "'#{file_reference_change["path"]}'."
600
+ return
568
601
  end
602
+
603
+ file_references.first
569
604
  end
570
605
 
571
- def find_reference_proxy(project, container_item_proxy_change)
606
+ def find_reference_proxy(project, container_item_proxy_change, reference_filter: ->(_) { true })
572
607
  reference_proxies = project.root_object.project_references.map do |project_ref_and_products|
573
608
  project_ref_and_products[:product_group].children.find do |product|
574
609
  product.remote_ref.remote_global_id_string ==
575
610
  container_item_proxy_change["remoteGlobalIDString"] &&
576
- product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"]
611
+ product.remote_ref.remote_info == container_item_proxy_change["remoteInfo"] &&
612
+ reference_filter.call(product)
577
613
  end
578
614
  end.compact
579
615
 
@@ -0,0 +1,105 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "optparse"
6
+
7
+ require_relative "version"
8
+
9
+ module Kintsugi
10
+ # Class resposible for creating the logic of various options for Kintsugi CLI.
11
+ class CLI
12
+ # Subcommands of Kintsugi CLI.
13
+ attr_reader :subcommands
14
+
15
+ # Root command Kintsugi CLI.
16
+ attr_reader :root_command
17
+
18
+ def initialize
19
+ @subcommands = {
20
+ "driver" => create_driver_subcommand
21
+ }.freeze
22
+ @root_command = create_root_command
23
+ end
24
+
25
+ private
26
+
27
+ Command = Struct.new(:option_parser, :action, :description, keyword_init: true)
28
+
29
+ def create_driver_subcommand
30
+ driver_option_parser =
31
+ OptionParser.new do |opts|
32
+ opts.banner = "Usage: kintsugi driver BASE OURS THEIRS ORIGINAL_FILE_PATH\n" \
33
+ "Uses Kintsugi as a Git merge driver. Parameters " \
34
+ "should be the path to base version of the file, path to ours version, path to " \
35
+ "theirs version, and the original file path."
36
+
37
+ opts.on("-h", "--help", "Prints this help") do
38
+ puts opts
39
+ exit
40
+ end
41
+ end
42
+
43
+ driver_action = lambda { |_, arguments, option_parser|
44
+ if arguments.count != 4
45
+ puts "Incorrect number of arguments to 'driver' subcommand\n\n"
46
+ puts option_parser
47
+ exit(1)
48
+ end
49
+ Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
50
+ }
51
+
52
+ Command.new(
53
+ option_parser: driver_option_parser,
54
+ action: driver_action,
55
+ description: "3-way merge compatible with Git merge driver"
56
+ )
57
+ end
58
+
59
+ def create_root_command
60
+ root_option_parser = OptionParser.new do |opts|
61
+ opts.banner = "Kintsugi, version #{Version::STRING}\n" \
62
+ "Copyright (c) 2021 Lightricks\n\n" \
63
+ "Usage: kintsugi <pbxproj_filepath> [options]\n" \
64
+ " kintsugi <subcommand> [options]"
65
+
66
+ opts.separator ""
67
+ opts.on("--changes-output-path=PATH", "Path to which changes applied to the project are " \
68
+ "written in JSON format. Used for debug purposes.")
69
+
70
+ opts.on("-h", "--help", "Prints this help") do
71
+ puts opts
72
+ exit
73
+ end
74
+
75
+ opts.on("-v", "--version", "Prints version") do
76
+ puts Version::STRING
77
+ exit
78
+ end
79
+
80
+ subcommands_descriptions = @subcommands.map do |command_name, command|
81
+ " #{command_name}: #{command.description}"
82
+ end.join("\n")
83
+ opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions}")
84
+ end
85
+
86
+ root_action = lambda { |options, arguments, option_parser|
87
+ if arguments.count != 1
88
+ puts "Incorrect number of arguments\n\n"
89
+ puts option_parser
90
+ exit(1)
91
+ end
92
+
93
+ project_file_path = File.expand_path(arguments[0])
94
+ Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
95
+ puts "Resolved conflicts successfully"
96
+ }
97
+
98
+ Command.new(
99
+ option_parser: root_option_parser,
100
+ action: root_action,
101
+ description: nil
102
+ )
103
+ end
104
+ end
105
+ end
@@ -0,0 +1,8 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Kintsugi
4
+ # This module holds the Kintsugi version information.
5
+ module Version
6
+ STRING = "0.3.1"
7
+ end
8
+ end
data/lib/kintsugi.rb CHANGED
@@ -2,6 +2,7 @@
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"
@@ -16,7 +17,7 @@ module Kintsugi
16
17
  # @param [String] project_file_path
17
18
  # Project to which to apply the changes.
18
19
  #
19
- # @param [String] output_changes_path
20
+ # @param [String] changes_output_path
20
21
  # Path to where the changes to apply to the project are written in JSON format.
21
22
  #
22
23
  # @raise [ArgumentError]
@@ -39,33 +40,68 @@ module Kintsugi
39
40
  File.write(changes_output_path, JSON.pretty_generate(change))
40
41
  end
41
42
 
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)
43
+ apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
44
+ end
50
45
 
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
46
+ # Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
47
+ # `ours_project_path`. The files may not be at the original path, and therefore the
48
+ # `original_project_path` is required in order for the project metadata to be written properly.
49
+ #
50
+ # @param [String] base_project_path
51
+ # Path to the base version of the project.
52
+ #
53
+ # @param [String] ours_project_path
54
+ # Path to ours version of the project.
55
+ #
56
+ # @param [String] theirs_project_path
57
+ # Path to theirs version of the project.
58
+ #
59
+ # @param [String] original_project_path
60
+ # Path to the original path of the file.
61
+ #
62
+ # @raise [RuntimeError]
63
+ # If there was an error applying the change to the project.
64
+ #
65
+ # @return [void]
66
+ def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
67
+ original_project_path)
68
+ original_directory_name = File.basename(File.dirname(original_project_path))
69
+ base_temporary_project =
70
+ copy_project_to_temporary_path_in_directory_with_name(base_project_path,
71
+ original_directory_name)
72
+ ours_temporary_project =
73
+ copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
74
+ original_directory_name)
75
+ theirs_temporary_project =
76
+ copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
77
+ original_directory_name)
78
+
79
+ change =
80
+ Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
81
+ :added, :removed)
82
+
83
+ apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
56
84
  end
57
85
 
58
86
  private
59
87
 
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
88
+ PROJECT_FILE_NAME = "project.pbxproj"
64
89
 
90
+ def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
91
+ apply_change_to_project(project, change)
92
+ project.save
93
+ FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
94
+ end
95
+
96
+ def validate_project(project_file_path)
65
97
  unless File.exist?(project_file_path)
66
98
  raise ArgumentError, "File '#{project_file_path}' doesn't exist"
67
99
  end
68
100
 
101
+ if File.extname(project_file_path) != ".pbxproj"
102
+ raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
103
+ end
104
+
69
105
  Dir.chdir(File.dirname(project_file_path)) do
70
106
  unless file_has_base_ours_and_theirs_versions?(project_file_path)
71
107
  raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, or a 3-way " \
@@ -74,8 +110,19 @@ module Kintsugi
74
110
  end
75
111
  end
76
112
 
113
+ def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
114
+ temp_directory_name = File.join(Dir.mktmpdir, directory_name)
115
+ Dir.mkdir(temp_directory_name)
116
+ temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
117
+ FileUtils.cp(project_file_path, temp_project_file_path)
118
+ Xcodeproj::Project.open(File.dirname(temp_project_file_path))
119
+ end
120
+
77
121
  def open_project_of_current_commit_in_temporary_directory(project_file_path)
78
- temp_project_file_path = File.join(Dir.mktmpdir, "project.pbxproj")
122
+ project_directory_name = File.basename(File.dirname(project_file_path))
123
+ temp_directory_name = File.join(Dir.mktmpdir, project_directory_name)
124
+ Dir.mkdir(temp_directory_name)
125
+ temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
79
126
  Dir.chdir(File.dirname(project_file_path)) do
80
127
  `git show HEAD:./project.pbxproj > #{temp_project_file_path}`
81
128
  end
@@ -99,11 +146,11 @@ module Kintsugi
99
146
 
100
147
  def change_of_conflicting_commit_with_parent(project_file_path)
101
148
  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}`
149
+ conflicting_commit_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
150
+ `git show :3:./#{PROJECT_FILE_NAME} > #{conflicting_commit_project_file_path}`
104
151
 
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}`
152
+ conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
153
+ `git show :1:./#{PROJECT_FILE_NAME} > #{conflicting_commit_parent_project_file_path}`
107
154
 
108
155
  conflicting_commit_project = Xcodeproj::Project.open(
109
156
  File.dirname(conflicting_commit_project_file_path)
@@ -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
 
@@ -770,7 +843,44 @@ describe Kintsugi, :apply_change_to_project do
770
843
  expect(base_project).to be_equivalent_to_project(theirs_project)
771
844
  end
772
845
 
773
- it "identifies subproject added in separate times" do
846
+ it "removes attribute target changes from a project it was removed from already" do
847
+ base_project.root_object.attributes["TargetAttributes"] =
848
+ {"foo" => {"LastSwiftMigration" => "1140"}}
849
+ base_project.save
850
+
851
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
852
+ theirs_project.root_object.attributes["TargetAttributes"]["foo"] = {}
853
+
854
+ ours_project = create_copy_of_project(base_project.path, "ours")
855
+ ours_project.root_object.attributes["TargetAttributes"]["foo"] = {}
856
+
857
+ changes_to_apply = get_diff(theirs_project, base_project)
858
+
859
+ described_class.apply_change_to_project(ours_project, changes_to_apply)
860
+ ours_project.save
861
+
862
+ expect(ours_project).to be_equivalent_to_project(theirs_project)
863
+ end
864
+
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
774
884
  framework_filename = "baz"
775
885
 
776
886
  subproject = new_subproject("subproj", framework_filename)
@@ -851,10 +961,10 @@ describe Kintsugi, :apply_change_to_project do
851
961
  file_reference.path == subproject_product_name
852
962
  end.remove_from_project
853
963
 
854
- project.root_object.project_references[0][:product_group] =
964
+ project.root_object.project_references[-1][:product_group] =
855
965
  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] <<
966
+ project.root_object.project_references[-1][:product_group].name = "Products"
967
+ project.root_object.project_references[-1][:product_group] <<
858
968
  create_reference_proxy_from_product_reference(project, subproject_reference,
859
969
  subproject.products_group.files[0])
860
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.1.0
4
+ version: 0.3.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-06-28 00:00:00.000000000 Z
11
+ date: 2021-11-30 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
@@ -116,7 +122,9 @@ files:
116
122
  - kintsugi.gemspec
117
123
  - lib/kintsugi.rb
118
124
  - lib/kintsugi/apply_change_to_project.rb
125
+ - lib/kintsugi/cli.rb
119
126
  - lib/kintsugi/utils.rb
127
+ - lib/kintsugi/version.rb
120
128
  - lib/kintsugi/xcodeproj_extensions.rb
121
129
  - logo/kintsugi.png
122
130
  - spec/be_equivalent_to_project.rb
@@ -141,7 +149,8 @@ required_rubygems_version: !ruby/object:Gem::Requirement
141
149
  - !ruby/object:Gem::Version
142
150
  version: '0'
143
151
  requirements: []
144
- rubygems_version: 3.1.2
152
+ rubyforge_project:
153
+ rubygems_version: 2.7.3
145
154
  signing_key:
146
155
  specification_version: 4
147
156
  summary: pbxproj files git conflicts solver