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.
- checksums.yaml +4 -4
- data/.github/workflows/release.yml +33 -0
- data/.github/workflows/{ci.yml → tests.yml} +2 -1
- data/.rubocop.yml +3 -0
- data/Gemfile +0 -5
- data/bin/kintsugi +2 -30
- data/kintsugi.gemspec +2 -1
- data/lib/kintsugi/apply_change_to_project.rb +141 -91
- data/lib/kintsugi/cli.rb +2 -1
- 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 +24 -7
@@ -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
|