kintsugi 0.5.0 → 0.5.4

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,146 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "json"
6
+ require "tmpdir"
7
+ require "tempfile"
8
+ require "xcodeproj"
9
+
10
+ require_relative "xcodeproj_extensions"
11
+ require_relative "apply_change_to_project"
12
+ require_relative "error"
13
+
14
+ module Kintsugi
15
+ class << self
16
+ # Resolves git conflicts of a pbxproj file specified by `project_file_path`.
17
+ #
18
+ # @param [String] project_file_path
19
+ # Project to which to apply the changes.
20
+ #
21
+ # @param [String] changes_output_path
22
+ # Path to where the changes to apply to the project are written in JSON format.
23
+ #
24
+ # @raise [ArgumentError]
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
27
+ #
28
+ # @raise [MergeError]
29
+ # If there was an error applying the change to the project.
30
+ #
31
+ # @return [void]
32
+ def resolve_conflicts(project_file_path, changes_output_path)
33
+ validate_project(project_file_path)
34
+
35
+ base_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 1)
36
+ ours_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 2)
37
+ theirs_project = copy_project_from_stage_number_to_temporary_directory(project_file_path, 3)
38
+
39
+ change = Xcodeproj::Differ.project_diff(theirs_project, base_project, :added, :removed)
40
+
41
+ if changes_output_path
42
+ File.write(changes_output_path, JSON.pretty_generate(change))
43
+ end
44
+
45
+ apply_change_and_copy_to_original_path(ours_project, change, project_file_path)
46
+ end
47
+
48
+ # Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
49
+ # `ours_project_path`. The files may not be at the original path, and therefore the
50
+ # `original_project_path` is required in order for the project metadata to be written properly.
51
+ #
52
+ # @param [String] base_project_path
53
+ # Path to the base version of the project.
54
+ #
55
+ # @param [String] ours_project_path
56
+ # Path to ours version of the project.
57
+ #
58
+ # @param [String] theirs_project_path
59
+ # Path to theirs version of the project.
60
+ #
61
+ # @param [String] original_project_path
62
+ # Path to the original path of the file.
63
+ #
64
+ # @raise [MergeError]
65
+ # If there was an error applying the change to the project.
66
+ #
67
+ # @return [void]
68
+ def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
69
+ original_project_path)
70
+ original_directory_name = File.basename(File.dirname(original_project_path))
71
+ base_temporary_project =
72
+ copy_project_to_temporary_path_in_directory_with_name(base_project_path,
73
+ original_directory_name)
74
+ ours_temporary_project =
75
+ copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
76
+ original_directory_name)
77
+ theirs_temporary_project =
78
+ copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
79
+ original_directory_name)
80
+
81
+ change =
82
+ Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
83
+ :added, :removed)
84
+
85
+ apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
86
+ end
87
+
88
+ private
89
+
90
+ PROJECT_FILE_NAME = "project.pbxproj"
91
+
92
+ def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
93
+ apply_change_to_project(project, change)
94
+ project.save
95
+ FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
96
+ end
97
+
98
+ def validate_project(project_file_path)
99
+ unless File.exist?(project_file_path)
100
+ raise ArgumentError, "File '#{project_file_path}' doesn't exist"
101
+ end
102
+
103
+ if File.extname(project_file_path) != ".pbxproj"
104
+ raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
105
+ end
106
+
107
+ unless file_has_base_ours_and_theirs_versions?(project_file_path)
108
+ raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
109
+ "or a 3-way merge is not possible."
110
+ end
111
+ end
112
+
113
+ def copy_project_from_stage_number_to_temporary_directory(project_file_path, stage_number)
114
+ project_directory_name = File.basename(File.dirname(project_file_path))
115
+ temp_project_file_path = File.join(Dir.mktmpdir, project_directory_name, PROJECT_FILE_NAME)
116
+ Dir.mkdir(File.dirname(temp_project_file_path))
117
+ Dir.chdir(File.dirname(project_file_path)) do
118
+ `git show :#{stage_number}:./#{PROJECT_FILE_NAME} > "#{temp_project_file_path}"`
119
+ end
120
+ Xcodeproj::Project.open(File.dirname(temp_project_file_path))
121
+ end
122
+
123
+ def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
124
+ temp_directory_name = File.join(Dir.mktmpdir, directory_name)
125
+ Dir.mkdir(temp_directory_name)
126
+ temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
127
+ FileUtils.cp(project_file_path, temp_project_file_path)
128
+ Xcodeproj::Project.open(File.dirname(temp_project_file_path))
129
+ end
130
+
131
+ def file_has_base_ours_and_theirs_versions?(file_path)
132
+ Dir.chdir(`git -C "#{File.dirname(file_path)}" rev-parse --show-toplevel`.strip) do
133
+ file_has_version_in_stage_numbers?(file_path, [1, 2, 3])
134
+ end
135
+ end
136
+
137
+ def file_has_version_in_stage_numbers?(file_path, stage_numbers)
138
+ file_absolute_path = File.absolute_path(file_path)
139
+ actual_stage_numbers =
140
+ `git ls-files -u -- "#{file_absolute_path}"`.split("\n").map do |git_file_status|
141
+ git_file_status.split[2]
142
+ end
143
+ (stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
144
+ end
145
+ end
146
+ 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.5.0"
6
+ STRING = "0.5.4"
7
7
  end
8
8
  end
@@ -66,4 +66,88 @@ module Xcodeproj
66
66
  end
67
67
  end
68
68
  end
69
+
70
+ module Differ
71
+ # Replaces the implementation of `array_diff` with an implementation that takes into account
72
+ # the number of occurrences an element is found in the array.
73
+ # Code was mostly copied from https://github.com/CocoaPods/Xcodeproj/blob/51fb78a03f31614103815ce21c56dc25c044a10d/lib/xcodeproj/differ.rb#L111
74
+ def self.array_diff(value_1, value_2, options)
75
+ ensure_class(value_1, Array)
76
+ ensure_class(value_2, Array)
77
+ return nil if value_1 == value_2
78
+
79
+ new_objects_value_1 = array_non_unique_diff(value_1, value_2)
80
+ new_objects_value_2 = array_non_unique_diff(value_2, value_1)
81
+ return nil if value_1.empty? && value_2.empty?
82
+
83
+ matched_diff = {}
84
+ if id_key = options[:id_key]
85
+ matched_value_1 = []
86
+ matched_value_2 = []
87
+ new_objects_value_1.each do |entry_value_1|
88
+ if entry_value_1.is_a?(Hash)
89
+ id_value = entry_value_1[id_key]
90
+ entry_value_2 = new_objects_value_2.find do |entry|
91
+ entry[id_key] == id_value
92
+ end
93
+ if entry_value_2
94
+ matched_value_1 << entry_value_1
95
+ matched_value_2 << entry_value_2
96
+ diff = diff(entry_value_1, entry_value_2, options)
97
+ matched_diff[id_value] = diff if diff
98
+ end
99
+ end
100
+ end
101
+
102
+ new_objects_value_1 -= matched_value_1
103
+ new_objects_value_2 -= matched_value_2
104
+ end
105
+
106
+ if new_objects_value_1.empty? && new_objects_value_2.empty?
107
+ if matched_diff.empty?
108
+ nil
109
+ else
110
+ matched_diff
111
+ end
112
+ else
113
+ result = {}
114
+ result[options[:key_1]] = new_objects_value_1 unless new_objects_value_1.empty?
115
+ result[options[:key_2]] = new_objects_value_2 unless new_objects_value_2.empty?
116
+ result[:diff] = matched_diff unless matched_diff.empty?
117
+ result
118
+ end
119
+ end
120
+
121
+ # Returns the difference between two arrays, taking into account the number of occurrences an
122
+ # element is found in both arrays.
123
+ #
124
+ # @param [Array] value_1
125
+ # First array to the difference operation.
126
+ #
127
+ # @param [Array] value_2
128
+ # Second array to the difference operation.
129
+ #
130
+ # @return [Array]
131
+ #
132
+ def self.array_non_unique_diff(value_1, value_2)
133
+ value_2_elements_by_count = value_2.reduce({}) do |hash, element|
134
+ updated_element_hash = hash.key?(element) ? {element => hash[element] + 1} : {element => 1}
135
+ hash.merge(updated_element_hash)
136
+ end
137
+
138
+ value_1_elements_by_deletions =
139
+ value_1.to_set.map do |element|
140
+ times_to_delete_element = value_2_elements_by_count[element] || 0
141
+ [element, times_to_delete_element]
142
+ end.to_h
143
+
144
+ value_1.select do |element|
145
+ if value_1_elements_by_deletions[element].positive?
146
+ value_1_elements_by_deletions[element] -= 1
147
+ next false
148
+ end
149
+ next true
150
+ end
151
+ end
152
+ end
69
153
  end
data/lib/kintsugi.rb CHANGED
@@ -1,167 +1,48 @@
1
- # Copyright (c) 2021 Lightricks. All rights reserved.
2
- # Created by Ben Yohay.
3
1
  # frozen_string_literal: true
4
2
 
5
- require "json"
6
- require "tmpdir"
7
- require "tempfile"
8
- require "xcodeproj"
3
+ # Copyright (c) 2022 Lightricks. All rights reserved.
4
+ # Created by Ben Yohay.
9
5
 
10
- require_relative "kintsugi/xcodeproj_extensions"
11
- require_relative "kintsugi/apply_change_to_project"
6
+ require_relative "kintsugi/cli"
12
7
  require_relative "kintsugi/error"
8
+ require_relative "kintsugi/merge"
13
9
 
14
10
  module Kintsugi
15
11
  class << self
16
- # Resolves git conflicts of a pbxproj file specified by `project_file_path`.
17
- #
18
- # @param [String] project_file_path
19
- # Project to which to apply the changes.
20
- #
21
- # @param [String] changes_output_path
22
- # Path to where the changes to apply to the project are written in JSON format.
23
- #
24
- # @raise [ArgumentError]
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
27
- #
28
- # @raise [MergeError]
29
- # If there was an error applying the change to the project.
30
- #
31
- # @return [void]
32
- def resolve_conflicts(project_file_path, changes_output_path)
33
- validate_project(project_file_path)
34
-
35
- project_in_temp_directory =
36
- open_project_of_current_commit_in_temporary_directory(project_file_path)
37
-
38
- change = change_of_conflicting_commit_with_parent(project_file_path)
39
-
40
- if changes_output_path
41
- File.write(changes_output_path, JSON.pretty_generate(change))
42
- end
43
-
44
- apply_change_and_copy_to_original_path(project_in_temp_directory, change, project_file_path)
45
- end
46
-
47
- # Merges the changes done between `theirs_project_path` and `base_project_path` to the file at
48
- # `ours_project_path`. The files may not be at the original path, and therefore the
49
- # `original_project_path` is required in order for the project metadata to be written properly.
50
- #
51
- # @param [String] base_project_path
52
- # Path to the base version of the project.
53
- #
54
- # @param [String] ours_project_path
55
- # Path to ours version of the project.
56
- #
57
- # @param [String] theirs_project_path
58
- # Path to theirs version of the project.
59
- #
60
- # @param [String] original_project_path
61
- # Path to the original path of the file.
62
- #
63
- # @raise [MergeError]
64
- # If there was an error applying the change to the project.
65
- #
66
- # @return [void]
67
- def three_way_merge(base_project_path, ours_project_path, theirs_project_path,
68
- original_project_path)
69
- original_directory_name = File.basename(File.dirname(original_project_path))
70
- base_temporary_project =
71
- copy_project_to_temporary_path_in_directory_with_name(base_project_path,
72
- original_directory_name)
73
- ours_temporary_project =
74
- copy_project_to_temporary_path_in_directory_with_name(ours_project_path,
75
- original_directory_name)
76
- theirs_temporary_project =
77
- copy_project_to_temporary_path_in_directory_with_name(theirs_project_path,
78
- original_directory_name)
79
-
80
- change =
81
- Xcodeproj::Differ.project_diff(theirs_temporary_project, base_temporary_project,
82
- :added, :removed)
83
-
84
- apply_change_and_copy_to_original_path(ours_temporary_project, change, ours_project_path)
85
- end
86
-
87
- private
88
-
89
- PROJECT_FILE_NAME = "project.pbxproj"
90
-
91
- def apply_change_and_copy_to_original_path(project, change, original_project_file_path)
92
- apply_change_to_project(project, change)
93
- project.save
94
- FileUtils.cp(File.join(project.path, PROJECT_FILE_NAME), original_project_file_path)
95
- end
96
-
97
- def validate_project(project_file_path)
98
- unless File.exist?(project_file_path)
99
- raise ArgumentError, "File '#{project_file_path}' doesn't exist"
100
- end
101
-
102
- if File.extname(project_file_path) != ".pbxproj"
103
- raise ArgumentError, "Wrong file extension, please provide file with extension .pbxproj\""
104
- end
105
-
106
- Dir.chdir(File.dirname(project_file_path)) do
107
- unless file_has_base_ours_and_theirs_versions?(project_file_path)
108
- raise ArgumentError, "File '#{project_file_path}' doesn't have conflicts, " \
109
- "or a 3-way merge is not possible."
12
+ def run(arguments)
13
+ first_argument = arguments[0]
14
+ cli = CLI.new
15
+ command =
16
+ if name_of_subcommand?(cli.subcommands, first_argument)
17
+ arguments.shift
18
+ cli.subcommands[first_argument]
19
+ else
20
+ cli.root_command
110
21
  end
111
- end
112
- end
113
22
 
114
- def copy_project_to_temporary_path_in_directory_with_name(project_file_path, directory_name)
115
- temp_directory_name = File.join(Dir.mktmpdir, directory_name)
116
- Dir.mkdir(temp_directory_name)
117
- temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
118
- FileUtils.cp(project_file_path, temp_project_file_path)
119
- Xcodeproj::Project.open(File.dirname(temp_project_file_path))
120
- end
23
+ options = parse_options!(command, arguments)
121
24
 
122
- def open_project_of_current_commit_in_temporary_directory(project_file_path)
123
- project_directory_name = File.basename(File.dirname(project_file_path))
124
- temp_directory_name = File.join(Dir.mktmpdir, project_directory_name)
125
- Dir.mkdir(temp_directory_name)
126
- temp_project_file_path = File.join(temp_directory_name, PROJECT_FILE_NAME)
127
- Dir.chdir(File.dirname(project_file_path)) do
128
- `git show HEAD:./project.pbxproj > "#{temp_project_file_path}"`
25
+ begin
26
+ command.action.call(options, arguments)
27
+ rescue ArgumentError => e
28
+ puts "#{e.class}: #{e}"
29
+ raise
30
+ rescue Kintsugi::MergeError => e
31
+ puts e
32
+ raise
129
33
  end
130
- Xcodeproj::Project.open(File.dirname(temp_project_file_path))
131
34
  end
132
35
 
133
- def file_has_base_ours_and_theirs_versions?(file_path)
134
- Dir.chdir(`git rev-parse --show-toplevel`.strip) do
135
- file_has_version_in_stage_numbers?(file_path, [1, 2, 3])
136
- end
137
- end
36
+ private
138
37
 
139
- def file_has_version_in_stage_numbers?(file_path, stage_numbers)
140
- file_absolute_path = File.absolute_path(file_path)
141
- actual_stage_numbers =
142
- `git ls-files -u -- "#{file_absolute_path}"`.split("\n").map do |git_file_status|
143
- git_file_status.split[2]
144
- end
145
- (stage_numbers - actual_stage_numbers.map(&:to_i)).empty?
38
+ def parse_options!(command, arguments)
39
+ options = {}
40
+ command.option_parser.parse!(arguments, into: options)
41
+ options
146
42
  end
147
43
 
148
- def change_of_conflicting_commit_with_parent(project_file_path)
149
- Dir.chdir(File.dirname(project_file_path)) do
150
- conflicting_commit_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
151
- `git show :3:./#{PROJECT_FILE_NAME} > #{conflicting_commit_project_file_path}`
152
-
153
- conflicting_commit_parent_project_file_path = File.join(Dir.mktmpdir, PROJECT_FILE_NAME)
154
- `git show :1:./#{PROJECT_FILE_NAME} > #{conflicting_commit_parent_project_file_path}`
155
-
156
- conflicting_commit_project = Xcodeproj::Project.open(
157
- File.dirname(conflicting_commit_project_file_path)
158
- )
159
- conflicting_commit_parent_project =
160
- Xcodeproj::Project.open(File.dirname(conflicting_commit_parent_project_file_path))
161
-
162
- Xcodeproj::Differ.project_diff(conflicting_commit_project,
163
- conflicting_commit_parent_project, :added, :removed)
164
- end
44
+ def name_of_subcommand?(subcommands, argument)
45
+ subcommands.include?(argument)
165
46
  end
166
47
  end
167
48
  end
@@ -427,7 +427,8 @@ describe Kintsugi, :apply_change_to_project do
427
427
 
428
428
  theirs_project = create_copy_of_project(base_project.path, "theirs")
429
429
 
430
- file_reference = theirs_project.main_group.new_reference(framework_filename)
430
+ file_reference = theirs_project.main_group.new_reference("bar")
431
+ file_reference.name = framework_filename
431
432
  build_phase = theirs_project.targets[0].frameworks_build_phase
432
433
  build_phase.files[-1].remove_from_project
433
434
  theirs_project.targets[0].frameworks_build_phase.add_file_reference(file_reference)
@@ -435,6 +436,10 @@ describe Kintsugi, :apply_change_to_project do
435
436
  changes_to_apply = get_diff(theirs_project, base_project)
436
437
 
437
438
  described_class.apply_change_to_project(base_project, changes_to_apply)
439
+ # This verifies we haven't created a new file reference instead of reusing the one in the
440
+ # hierarchy.
441
+ base_project.files[-1].name = "foo"
442
+ theirs_project.files[-1].name = "foo"
438
443
  base_project.save
439
444
 
440
445
  expect(base_project).to be_equivalent_to_project(theirs_project)
@@ -531,7 +536,7 @@ describe Kintsugi, :apply_change_to_project do
531
536
  expect(base_project).to be_equivalent_to_project(theirs_project, ignore_keys: ["containerPortal"])
532
537
  end
533
538
 
534
- it "adds build file to a file reference that already exist" do
539
+ it "adds build file to a file reference that already exists" do
535
540
  base_project.main_group.new_reference("bar")
536
541
 
537
542
  base_project.main_group.new_reference("bar")
@@ -1062,6 +1067,20 @@ describe Kintsugi, :apply_change_to_project do
1062
1067
  expect(base_project).to be_equivalent_to_project(theirs_project)
1063
1068
  end
1064
1069
 
1070
+ it "adds build phase with a simple attribute value that has non nil default" do
1071
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
1072
+
1073
+ theirs_project.targets[0].new_shell_script_build_phase("bar")
1074
+ theirs_project.targets[0].build_phases.last.shell_script = "Other value"
1075
+
1076
+ changes_to_apply = get_diff(theirs_project, base_project)
1077
+
1078
+ described_class.apply_change_to_project(base_project, changes_to_apply)
1079
+ base_project.save
1080
+
1081
+ expect(base_project).to be_equivalent_to_project(theirs_project)
1082
+ end
1083
+
1065
1084
  it "removes build phase" do
1066
1085
  base_project.targets[0].new_shell_script_build_phase("bar")
1067
1086
 
@@ -1346,6 +1365,41 @@ describe Kintsugi, :apply_change_to_project do
1346
1365
  expect(base_project).to be_equivalent_to_project(theirs_project)
1347
1366
  end
1348
1367
 
1368
+ it "adds group to product group" do
1369
+ base_project_path = make_temp_directory("base", ".xcodeproj")
1370
+ base_project = Xcodeproj::Project.new(base_project_path)
1371
+ base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
1372
+
1373
+ base_project.save
1374
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
1375
+
1376
+ theirs_project.root_object.product_ref_group.new_group("foo")
1377
+
1378
+ changes_to_apply = get_diff(theirs_project, base_project)
1379
+
1380
+ described_class.apply_change_to_project(base_project, changes_to_apply)
1381
+
1382
+ expect(base_project).to be_equivalent_to_project(theirs_project)
1383
+ end
1384
+
1385
+ it "adds localization files to product group" do
1386
+ base_project_path = make_temp_directory("base", ".xcodeproj")
1387
+ base_project = Xcodeproj::Project.new(base_project_path)
1388
+ base_project.new_target("com.apple.product-type.library.static", "foo", :ios)
1389
+
1390
+ base_project.save
1391
+ theirs_project = create_copy_of_project(base_project.path, "theirs")
1392
+
1393
+ variant_group = theirs_project.root_object.product_ref_group.new_variant_group("foo.strings")
1394
+ variant_group.new_reference("Base").last_known_file_type = "text.plist.strings"
1395
+
1396
+ changes_to_apply = get_diff(theirs_project, base_project)
1397
+
1398
+ described_class.apply_change_to_project(base_project, changes_to_apply)
1399
+
1400
+ expect(base_project).to be_equivalent_to_project(theirs_project)
1401
+ end
1402
+
1349
1403
  def create_copy_of_project(project_path, new_project_prefix)
1350
1404
  copied_project_path = make_temp_directory(new_project_prefix, ".xcodeproj")
1351
1405
  FileUtils.cp(File.join(project_path, "project.pbxproj"), copied_project_path)
@@ -0,0 +1,148 @@
1
+ # Copyright (c) 2021 Lightricks. All rights reserved.
2
+ # Created by Ben Yohay.
3
+ # frozen_string_literal: true
4
+
5
+ require "git"
6
+ require "json"
7
+ require "rspec"
8
+ require "tempfile"
9
+ require "tmpdir"
10
+
11
+ require "kintsugi"
12
+
13
+ shared_examples "tests" do |git_command, project_name|
14
+ let(:temporary_directories_paths) { [] }
15
+ let(:git_directory_path) { make_temp_directory }
16
+ let(:git) { Git.init(git_directory_path) }
17
+
18
+ before do
19
+ git.config("user.email", "you@example.com")
20
+ git.config("user.name", "Your Name")
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
+ context "running 'git #{git_command}' with project name '#{project_name}'" do
30
+ it "resolves conflicts with root command" do
31
+ File.write(File.join(git_directory_path, ".gitattributes"), "*.pbxproj merge=Unset")
32
+
33
+ project = create_new_project_at_path(File.join(git_directory_path, project_name))
34
+
35
+ git.add(File.join(git_directory_path, ".gitattributes"))
36
+ git.add(project.path)
37
+ git.commit("Initial project")
38
+
39
+ project.new_target("com.apple.product-type.library.static", "foo", :ios)
40
+ project.save
41
+
42
+ git.add(all: true)
43
+ git.commit("Add target foo")
44
+ first_commit_hash = git.revparse("HEAD")
45
+
46
+ git.checkout("HEAD^")
47
+ project = Xcodeproj::Project.open(project.path)
48
+ project.new_target("com.apple.product-type.library.static", "bar", :ios)
49
+ project.save
50
+ git.add(all: true)
51
+ git.commit("Add target bar")
52
+
53
+ `git -C #{git_directory_path} #{git_command} #{first_commit_hash} &> /dev/null`
54
+ Kintsugi.run([File.join(project.path, "project.pbxproj")])
55
+
56
+ project = Xcodeproj::Project.open(project.path)
57
+ expect(project.targets.map(&:display_name)).to contain_exactly("foo", "bar")
58
+ end
59
+
60
+ it "resolves conflicts automatically with driver" do
61
+ git.config("merge.kintsugi.name", "Kintsugi driver")
62
+ git.config("merge.kintsugi.driver", "#{__dir__}/../bin/kintsugi driver %O %A %B %P")
63
+ File.write(File.join(git_directory_path, ".gitattributes"), "*.pbxproj merge=kintsugi")
64
+
65
+ project = create_new_project_at_path(File.join(git_directory_path, project_name))
66
+
67
+ git.add(File.join(git_directory_path, ".gitattributes"))
68
+ git.add(project.path)
69
+ git.commit("Initial project")
70
+
71
+ project.new_target("com.apple.product-type.library.static", "foo", :ios)
72
+ project.save
73
+
74
+ git.add(all: true)
75
+ git.commit("Add target foo")
76
+ first_commit_hash = git.revparse("HEAD")
77
+
78
+ git.checkout("HEAD^")
79
+ project = Xcodeproj::Project.open(project.path)
80
+ project.new_target("com.apple.product-type.library.static", "bar", :ios)
81
+ project.save
82
+ git.add(all: true)
83
+ git.commit("Add target bar")
84
+
85
+ `git -C #{git_directory_path} #{git_command} #{first_commit_hash} &> /dev/null`
86
+
87
+ project = Xcodeproj::Project.open(project.path)
88
+ expect(project.targets.map(&:display_name)).to contain_exactly("foo", "bar")
89
+ end
90
+
91
+ it "keeps conflicts if failed to resolve conflicts" do
92
+ File.write(File.join(git_directory_path, ".gitattributes"), "*.pbxproj merge=Unset")
93
+
94
+ project = create_new_project_at_path(File.join(git_directory_path, project_name))
95
+ project.new_target("com.apple.product-type.library.static", "foo", :ios)
96
+ project.save
97
+
98
+ git.add(File.join(git_directory_path, ".gitattributes"))
99
+ git.add(project.path)
100
+ git.commit("Initial project")
101
+
102
+ project.targets[0].build_configurations.each do |configuration|
103
+ configuration.build_settings["PRODUCT_NAME"] = "bar"
104
+ end
105
+ project.save
106
+ git.add(all: true)
107
+ git.commit("Change target product name to bar")
108
+ first_commit_hash = git.revparse("HEAD")
109
+
110
+ git.checkout("HEAD^")
111
+ project = Xcodeproj::Project.open(project.path)
112
+ project.targets[0].build_configurations.each do |configuration|
113
+ configuration.build_settings["PRODUCT_NAME"] = "baz"
114
+ end
115
+ project.save
116
+ git.add(all: true)
117
+ git.commit("Change target product name to baz")
118
+
119
+ `git -C #{git_directory_path} #{git_command} #{first_commit_hash} &> /dev/null`
120
+
121
+ expect {
122
+ Kintsugi.run([File.join(project.path, "project.pbxproj")])
123
+ }.to raise_error(Kintsugi::MergeError)
124
+ expect(`git -C #{git_directory_path} diff --name-only --diff-filter=U`.chomp)
125
+ .to eq("#{project_name}/project.pbxproj")
126
+ end
127
+ end
128
+
129
+ def make_temp_directory
130
+ directory_path = Dir.mktmpdir
131
+ temporary_directories_paths << directory_path
132
+ directory_path
133
+ end
134
+ end
135
+
136
+ def create_new_project_at_path(path)
137
+ project = Xcodeproj::Project.new(path)
138
+ project.save
139
+ project
140
+ end
141
+
142
+ describe Kintsugi, :kintsugi do
143
+ %w[rebase cherry-pick merge].each do |git_command|
144
+ ["foo.xcodeproj", "foo with space.xcodeproj"].each do |project_name|
145
+ it_behaves_like("tests", git_command, project_name)
146
+ end
147
+ end
148
+ end