kintsugi 0.5.2 → 0.5.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +8 -0
- data/.github/workflows/tests.yml +1 -0
- data/.rubocop.yml +3 -0
- data/bin/kintsugi +2 -30
- data/kintsugi.gemspec +1 -0
- data/lib/kintsugi/apply_change_to_project.rb +141 -91
- data/lib/kintsugi/cli.rb +1 -0
- data/lib/kintsugi/merge.rb +146 -0
- data/lib/kintsugi/version.rb +1 -1
- data/lib/kintsugi/xcodeproj_extensions.rb +84 -0
- data/lib/kintsugi.rb +29 -148
- data/spec/kintsugi_apply_change_to_project_spec.rb +56 -2
- data/spec/kintsugi_integration_spec.rb +148 -0
- metadata +20 -3
@@ -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
|
data/lib/kintsugi/version.rb
CHANGED
@@ -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
|
-
|
6
|
-
|
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/
|
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
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
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
|
-
|
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
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
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
|
-
|
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
|
140
|
-
|
141
|
-
|
142
|
-
|
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
|
149
|
-
|
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(
|
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
|
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
|