kintsugi 0.5.2 → 0.5.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -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.2"
6
+ STRING = "0.5.3"
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