kintsugi 0.5.2 → 0.6.0
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 +2 -1
- data/lib/kintsugi/apply_change_to_project.rb +382 -135
- data/lib/kintsugi/cli.rb +8 -0
- data/lib/kintsugi/merge.rb +146 -0
- data/lib/kintsugi/settings.rb +18 -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 +346 -111
- data/spec/kintsugi_integration_spec.rb +148 -0
- metadata +23 -5
data/lib/kintsugi/cli.rb
CHANGED
@@ -5,6 +5,7 @@
|
|
5
5
|
require "fileutils"
|
6
6
|
require "optparse"
|
7
7
|
|
8
|
+
require_relative "settings"
|
8
9
|
require_relative "version"
|
9
10
|
|
10
11
|
module Kintsugi
|
@@ -50,6 +51,7 @@ module Kintsugi
|
|
50
51
|
exit(1)
|
51
52
|
end
|
52
53
|
Kintsugi.three_way_merge(arguments[0], arguments[1], arguments[2], arguments[3])
|
54
|
+
warn "\e[32mKintsugi auto-merged #{arguments[3]}\e[0m"
|
53
55
|
}
|
54
56
|
|
55
57
|
Command.new(
|
@@ -181,6 +183,8 @@ module Kintsugi
|
|
181
183
|
exit
|
182
184
|
end
|
183
185
|
|
186
|
+
opts.on("--allow-duplicates", "Allow to add duplicates of the same entity")
|
187
|
+
|
184
188
|
opts.on_tail("\nSUBCOMMANDS\n#{subcommands_descriptions(subcommands)}")
|
185
189
|
end
|
186
190
|
|
@@ -191,6 +195,10 @@ module Kintsugi
|
|
191
195
|
exit(1)
|
192
196
|
end
|
193
197
|
|
198
|
+
if options[:"allow-duplicates"]
|
199
|
+
Settings.allow_duplicates = true
|
200
|
+
end
|
201
|
+
|
194
202
|
project_file_path = File.expand_path(arguments[0])
|
195
203
|
Kintsugi.resolve_conflicts(project_file_path, options[:"changes-output-path"])
|
196
204
|
puts "Resolved conflicts successfully"
|
@@ -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
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# Copyright (c) 2022 Lightricks. All rights reserved.
|
2
|
+
# Created by Ben Yohay.
|
3
|
+
# frozen_string_literal: true
|
4
|
+
|
5
|
+
module Kintsugi
|
6
|
+
# Kintsugi global settings.
|
7
|
+
class Settings
|
8
|
+
class << self
|
9
|
+
# `true` if Kintsugi can create entities that are identical to existing ones, `false`
|
10
|
+
# otherwise.
|
11
|
+
attr_writer :allow_duplicates
|
12
|
+
|
13
|
+
def allow_duplicates
|
14
|
+
@allow_duplicates || false
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
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
|