toml-merge 7.1.1 → 7.1.3
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
- checksums.yaml.gz.sig +0 -0
- data/README.md +2 -10
- data/lib/toml/merge/provider.rb +25 -0
- data/lib/toml/merge/source_preserving_provider.rb +871 -0
- data/lib/toml/merge/version.rb +1 -1
- data/lib/toml/merge.rb +3 -0
- data/sig/toml/merge.rbs +21 -0
- data.tar.gz.sig +0 -0
- metadata +18 -16
- metadata.gz.sig +0 -0
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 1897d1ad4cb680eef623f8a329d9c0c917345e30f9997e340e91ae35e9f57d01
|
|
4
|
+
data.tar.gz: e858428956c256aef47413f8efe5d9aa01b246ef160527b14d02857e4b3ac18f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 19015402b3398c6d2ea9babedd05a76b2b7d681f946f83eae5fc32b5097f2f3fa3bd97c0e0736c9c4c0753b4ced6a8de670c96d77ce0f8d8bba8d224bbb0916d
|
|
7
|
+
data.tar.gz: 142922345fc8ba7158cc809f082453789b8b5fa3553628f06703b6d49b46834c0d48c61abb006cc33d08266edc4f671c36aa9870da4dbb70667abfaa7933f043
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -90,7 +90,7 @@ puts result.content if result.success?
|
|
|
90
90
|
|
|
91
91
|
### Compatibility
|
|
92
92
|
|
|
93
|
-
Compatible with MRI Ruby 4.0.0+,
|
|
93
|
+
Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
|
|
94
94
|
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
|
|
95
95
|
This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
|
|
96
96
|
may be higher than the gem's runtime compatibility floor when legacy Rubies are
|
|
@@ -178,20 +178,12 @@ See [SECURITY.md][🔐security].
|
|
|
178
178
|
## 🤝 Contributing
|
|
179
179
|
|
|
180
180
|
If you need some ideas of where to help, you could work on adding more code coverage,
|
|
181
|
-
|
|
182
|
-
or use the gem and think about how it could be better.
|
|
181
|
+
check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
|
|
183
182
|
|
|
184
183
|
We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
|
|
185
184
|
|
|
186
185
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
187
186
|
|
|
188
|
-
### Code Coverage
|
|
189
|
-
|
|
190
|
-
<details markdown="1">
|
|
191
|
-
<summary>Coverage service badges</summary>
|
|
192
|
-
|
|
193
|
-
</details>
|
|
194
|
-
|
|
195
187
|
## 📌 Versioning
|
|
196
188
|
|
|
197
189
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Toml
|
|
4
|
+
# Portable TOML workflow integration.
|
|
5
|
+
module Merge
|
|
6
|
+
# Source-preserving TOML workflow configured for kreuzberg-language-pack.
|
|
7
|
+
class Provider < SourcePreservingProvider
|
|
8
|
+
PROVIDER_ID = 'ruby.toml'
|
|
9
|
+
BACKEND = TREE_SITTER_BACKEND_REFERENCE
|
|
10
|
+
PACKAGE_NAME = Merge::PACKAGE_NAME
|
|
11
|
+
PACKAGE_VERSION = Version::VERSION
|
|
12
|
+
ROLE = :workflow
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
class << self
|
|
16
|
+
def merge_provider
|
|
17
|
+
@merge_provider ||= Provider.new
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def register_provider!(replace: false)
|
|
21
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
@@ -0,0 +1,871 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Toml
|
|
6
|
+
module Merge
|
|
7
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider validation, planning, rendering, and verification form one safety boundary
|
|
8
|
+
# Backend-parameterized conservative source-preserving provider for top-level TOML mappings.
|
|
9
|
+
class SourcePreservingProvider
|
|
10
|
+
DEFAULT_PROFILE = :source_preserving
|
|
11
|
+
DIALECTS = %i[toml].freeze
|
|
12
|
+
|
|
13
|
+
Entry = Data.define(
|
|
14
|
+
:key,
|
|
15
|
+
:start_line,
|
|
16
|
+
:end_line,
|
|
17
|
+
:fragment,
|
|
18
|
+
:semantic,
|
|
19
|
+
:attributes,
|
|
20
|
+
:role,
|
|
21
|
+
:ownership_provable
|
|
22
|
+
)
|
|
23
|
+
Document = Data.define(
|
|
24
|
+
:role,
|
|
25
|
+
:source,
|
|
26
|
+
:analysis,
|
|
27
|
+
:entries,
|
|
28
|
+
:by_key,
|
|
29
|
+
:issues,
|
|
30
|
+
:document_attributes
|
|
31
|
+
)
|
|
32
|
+
Decision = Data.define(:key, :base, :ours, :theirs, :selected_role, :classification, :conflict)
|
|
33
|
+
|
|
34
|
+
def provider_id = self.class::PROVIDER_ID
|
|
35
|
+
def family = 'toml'
|
|
36
|
+
def backend_reference = self.class::BACKEND
|
|
37
|
+
def backend_id = backend_reference.id
|
|
38
|
+
def package_name = self.class::PACKAGE_NAME
|
|
39
|
+
def package_version = self.class::PACKAGE_VERSION
|
|
40
|
+
def role = self.class.const_defined?(:ROLE, false) ? self.class::ROLE : :backend
|
|
41
|
+
|
|
42
|
+
def capabilities
|
|
43
|
+
{
|
|
44
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
45
|
+
dialects: DIALECTS,
|
|
46
|
+
backends: [backend_id.to_sym],
|
|
47
|
+
profiles: [DEFAULT_PROFILE],
|
|
48
|
+
role: role,
|
|
49
|
+
source_preservation: %i[
|
|
50
|
+
exact_source
|
|
51
|
+
mapping_entry_fragments
|
|
52
|
+
comment_ownership
|
|
53
|
+
line_provenance
|
|
54
|
+
reparse
|
|
55
|
+
semantic_verification
|
|
56
|
+
ast_attribute_verification
|
|
57
|
+
]
|
|
58
|
+
}.freeze
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def analyze(request)
|
|
62
|
+
document = analyze_role(:source, request.fetch(:source))
|
|
63
|
+
return invalid_document_failure(:analyze, request, [document]) unless document.issues.empty?
|
|
64
|
+
|
|
65
|
+
result(
|
|
66
|
+
:analyze,
|
|
67
|
+
request,
|
|
68
|
+
analysis: analysis_payload(document),
|
|
69
|
+
verification: { source_parsed: true, structurally_unambiguous: true }
|
|
70
|
+
)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def diff2(request)
|
|
74
|
+
before = analyze_role(:before, request.fetch(:before_source))
|
|
75
|
+
after = analyze_role(:after, request.fetch(:after_source))
|
|
76
|
+
unsafe = [before, after].reject { |document| document.issues.empty? }
|
|
77
|
+
return invalid_document_failure(:diff2, request, unsafe) unless unsafe.empty?
|
|
78
|
+
|
|
79
|
+
changes = mapping_changes(before, after)
|
|
80
|
+
result(
|
|
81
|
+
:diff2,
|
|
82
|
+
request,
|
|
83
|
+
diff: { before: analysis_payload(before), after: analysis_payload(after), changes: changes },
|
|
84
|
+
changes: changes,
|
|
85
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def merge2(request)
|
|
90
|
+
documents = {
|
|
91
|
+
incoming: analyze_role(:incoming, request.fetch(:incoming_source)),
|
|
92
|
+
current: analyze_role(:current, request.fetch(:current_source))
|
|
93
|
+
}
|
|
94
|
+
unsafe = documents.values.reject { |document| document.issues.empty? }
|
|
95
|
+
return invalid_document_failure(:merge2, request, unsafe) unless unsafe.empty?
|
|
96
|
+
|
|
97
|
+
additions = documents[:incoming].entries.reject { |entry| documents[:current].by_key.key?(entry.key) }
|
|
98
|
+
render_documents = { ours: documents[:current], theirs: documents[:incoming] }
|
|
99
|
+
fragments = [whole_source_fragment(:ours, documents[:current].source)]
|
|
100
|
+
fragments.concat(additions.map { |entry| entry_fragment(:theirs, entry) })
|
|
101
|
+
rendered = render_plan(request, separated_fragments(fragments, render_documents))
|
|
102
|
+
expected = [
|
|
103
|
+
*documents[:current].entries.map { |entry| selected_entry(entry, :current) },
|
|
104
|
+
*additions.map { |entry| selected_entry(entry, :incoming) }
|
|
105
|
+
]
|
|
106
|
+
verification = verify_composite(rendered.content, expected)
|
|
107
|
+
return verification_failure(:merge2, request, rendered, [], verification) unless verification[:semantic_match]
|
|
108
|
+
|
|
109
|
+
result(
|
|
110
|
+
:merge2,
|
|
111
|
+
request,
|
|
112
|
+
output: rendered.content,
|
|
113
|
+
changes: mapping_changes(documents[:current], analyze_role(:output, rendered.content)),
|
|
114
|
+
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
115
|
+
verification: verification
|
|
116
|
+
)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
def merge3(request)
|
|
120
|
+
documents = %i[base ours theirs].to_h do |role|
|
|
121
|
+
[role, analyze_role(role, request.fetch(:"#{role}_source"))]
|
|
122
|
+
end
|
|
123
|
+
exact_role = exact_revision_role(documents)
|
|
124
|
+
return render_exact(request, documents, exact_role) if exact_role
|
|
125
|
+
|
|
126
|
+
unsafe = documents.values.reject { |document| document.issues.empty? }
|
|
127
|
+
return structural_conflict(request, documents, unsafe) unless unsafe.empty?
|
|
128
|
+
return unmanaged_conflict(request, documents) unless unmanaged_unchanged?(documents)
|
|
129
|
+
|
|
130
|
+
decisions = three_way_decisions(documents)
|
|
131
|
+
conflicts = decisions.select(&:conflict)
|
|
132
|
+
return render_conflicts(request, documents, decisions, conflicts) unless conflicts.empty?
|
|
133
|
+
|
|
134
|
+
render_composite(request, documents, decisions)
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
private
|
|
138
|
+
|
|
139
|
+
def analyze_role(role, source)
|
|
140
|
+
analysis = TreeHaver.with_backend(backend_id) { ::Toml::Merge::FileAnalysis.new(source) }
|
|
141
|
+
unless analysis.valid?
|
|
142
|
+
message = analysis.errors.map(&:to_s).join('; ')
|
|
143
|
+
return invalid_document(role, source, :parse_error, message.empty? ? 'TOML parse failed' : message)
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
issues = []
|
|
147
|
+
if (table = analysis.tables.find(&:table?))
|
|
148
|
+
issues << issue(
|
|
149
|
+
:table,
|
|
150
|
+
'TOML tables require an exact one-sided merge',
|
|
151
|
+
source_lines: [table.start_line, table.effective_end_line]
|
|
152
|
+
)
|
|
153
|
+
end
|
|
154
|
+
if (table_array = analysis.tables.find(&:array_of_tables?))
|
|
155
|
+
issues << issue(
|
|
156
|
+
:array_of_tables,
|
|
157
|
+
'TOML arrays of tables require an exact one-sided merge',
|
|
158
|
+
source_lines: [table_array.start_line, table_array.effective_end_line]
|
|
159
|
+
)
|
|
160
|
+
end
|
|
161
|
+
entries = mapping_entries(analysis, source, role)
|
|
162
|
+
duplicate = entries.group_by(&:key).find { |_key, matches| matches.length > 1 }
|
|
163
|
+
if duplicate
|
|
164
|
+
issues << issue(
|
|
165
|
+
:duplicate_key,
|
|
166
|
+
"Duplicate TOML key #{duplicate.first}",
|
|
167
|
+
key: duplicate.first,
|
|
168
|
+
source_lines: duplicate.last.map { |entry| [entry.start_line, entry.end_line] }
|
|
169
|
+
)
|
|
170
|
+
end
|
|
171
|
+
entries.each do |entry|
|
|
172
|
+
if entry.key.include?('.')
|
|
173
|
+
issues << issue(
|
|
174
|
+
:dotted_key,
|
|
175
|
+
"TOML key #{entry.key} has ambiguous ownership",
|
|
176
|
+
key: entry.key,
|
|
177
|
+
source_lines: [entry.start_line, entry.end_line]
|
|
178
|
+
)
|
|
179
|
+
end
|
|
180
|
+
next if entry.ownership_provable
|
|
181
|
+
|
|
182
|
+
issues << issue(
|
|
183
|
+
:unprovable_range,
|
|
184
|
+
"TOML key #{entry.key} has no provable source range",
|
|
185
|
+
key: entry.key,
|
|
186
|
+
source_lines: [entry.start_line, entry.end_line]
|
|
187
|
+
)
|
|
188
|
+
end
|
|
189
|
+
|
|
190
|
+
Document.new(
|
|
191
|
+
role: role,
|
|
192
|
+
source: source,
|
|
193
|
+
analysis: analysis,
|
|
194
|
+
entries: entries.freeze,
|
|
195
|
+
by_key: entries.to_h { |entry| [entry.key, entry] }.freeze,
|
|
196
|
+
issues: issues.freeze,
|
|
197
|
+
document_attributes: node_attributes(analysis.root_node)
|
|
198
|
+
)
|
|
199
|
+
rescue TreeHaver::Error => e
|
|
200
|
+
invalid_document(role, source, :parse_error, e.message)
|
|
201
|
+
rescue StandardError => e
|
|
202
|
+
invalid_document(role, source, :structural_error, e.message)
|
|
203
|
+
end
|
|
204
|
+
|
|
205
|
+
def invalid_document(role, source, category, message)
|
|
206
|
+
Document.new(
|
|
207
|
+
role: role,
|
|
208
|
+
source: source,
|
|
209
|
+
analysis: nil,
|
|
210
|
+
entries: [].freeze,
|
|
211
|
+
by_key: {}.freeze,
|
|
212
|
+
issues: [issue(category, message)].freeze,
|
|
213
|
+
document_attributes: {}
|
|
214
|
+
)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def mapping_entries(analysis, source, role)
|
|
218
|
+
analysis.root_pairs.filter_map.with_index do |pair, index|
|
|
219
|
+
build_entry(analysis, pair, source, role, index)
|
|
220
|
+
end
|
|
221
|
+
end
|
|
222
|
+
|
|
223
|
+
def build_entry(analysis, pair, source, role, index)
|
|
224
|
+
key = pair.key_name
|
|
225
|
+
value = pair.value_node
|
|
226
|
+
key_line = pair.start_line
|
|
227
|
+
value_end = pair.end_line
|
|
228
|
+
return unless key && value && key_line && value_end && value_end >= key_line
|
|
229
|
+
|
|
230
|
+
leading_start = owned_leading_start(analysis, pair, index, key_line)
|
|
231
|
+
fragment = source_lines(source, leading_start, value_end)
|
|
232
|
+
Entry.new(
|
|
233
|
+
key: key,
|
|
234
|
+
start_line: leading_start,
|
|
235
|
+
end_line: value_end,
|
|
236
|
+
fragment: fragment,
|
|
237
|
+
semantic: node_semantic(value),
|
|
238
|
+
attributes: node_attributes(value),
|
|
239
|
+
role: role,
|
|
240
|
+
ownership_provable: ownership_provable?(analysis, pair, index, key_line, leading_start)
|
|
241
|
+
)
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
def owned_leading_start(analysis, pair, index, key_line)
|
|
245
|
+
return key_line if index.zero?
|
|
246
|
+
|
|
247
|
+
region = analysis.comment_attachment_for(pair)&.leading_region
|
|
248
|
+
return key_line unless region && region.end_line == key_line - 1
|
|
249
|
+
return key_line if region.metadata[:floating]
|
|
250
|
+
|
|
251
|
+
region.start_line
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
def ownership_provable?(analysis, pair, index, key_line, leading_start)
|
|
255
|
+
return true if leading_start == key_line
|
|
256
|
+
return false if index.zero?
|
|
257
|
+
|
|
258
|
+
region = analysis.comment_attachment_for(pair)&.leading_region
|
|
259
|
+
region && !region.metadata[:floating] && region.start_line == leading_start
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def node_semantic(node)
|
|
263
|
+
{
|
|
264
|
+
type: node.canonical_type.to_s,
|
|
265
|
+
source_value: node.text
|
|
266
|
+
}.freeze
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def node_attributes(node)
|
|
270
|
+
return {} unless node
|
|
271
|
+
|
|
272
|
+
{
|
|
273
|
+
type: node.type.to_s,
|
|
274
|
+
canonical_type: node.canonical_type.to_s,
|
|
275
|
+
signature: public_value(node.signature)
|
|
276
|
+
}.freeze
|
|
277
|
+
end
|
|
278
|
+
|
|
279
|
+
def public_value(value)
|
|
280
|
+
case value
|
|
281
|
+
when Array then value.map { |item| public_value(item) }
|
|
282
|
+
when Hash then value.to_h { |key, item| [key.to_s, public_value(item)] }
|
|
283
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass then value
|
|
284
|
+
else value.to_s
|
|
285
|
+
end
|
|
286
|
+
end
|
|
287
|
+
|
|
288
|
+
def analysis_payload(document)
|
|
289
|
+
{
|
|
290
|
+
dialect: 'toml',
|
|
291
|
+
backend: backend_id,
|
|
292
|
+
document_attributes: document.document_attributes,
|
|
293
|
+
entries: document.entries.map do |entry|
|
|
294
|
+
{
|
|
295
|
+
key: entry.key,
|
|
296
|
+
value: entry.semantic,
|
|
297
|
+
attributes: entry.attributes,
|
|
298
|
+
source_role: document.role,
|
|
299
|
+
source_lines: [entry.start_line, entry.end_line]
|
|
300
|
+
}
|
|
301
|
+
end,
|
|
302
|
+
structurally_unambiguous: document.issues.empty?
|
|
303
|
+
}
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
def entry_state(entry)
|
|
307
|
+
entry && [entry.semantic, entry.attributes, entry.fragment]
|
|
308
|
+
end
|
|
309
|
+
|
|
310
|
+
def exact_revision_role(documents)
|
|
311
|
+
return :ours if documents[:ours].source == documents[:theirs].source
|
|
312
|
+
return :ours if documents[:base].source == documents[:theirs].source
|
|
313
|
+
|
|
314
|
+
:theirs if documents[:base].source == documents[:ours].source
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
def render_exact(request, documents, role)
|
|
318
|
+
winner = documents.fetch(role)
|
|
319
|
+
return structural_conflict(request, documents, [winner]) unless fatal_issues(winner).empty?
|
|
320
|
+
|
|
321
|
+
rendered = render_plan(request, [whole_source_fragment(role, winner.source)])
|
|
322
|
+
verification = verify_exact(rendered.content, winner, role)
|
|
323
|
+
return verification_failure(:merge3, request, rendered, [], verification) unless verification[:semantic_match]
|
|
324
|
+
|
|
325
|
+
result(
|
|
326
|
+
:merge3,
|
|
327
|
+
request,
|
|
328
|
+
output: rendered.content,
|
|
329
|
+
changes: mapping_changes(documents[:base], winner),
|
|
330
|
+
diagnostics: diagnostics_for(*documents.values),
|
|
331
|
+
render_report: render_report(rendered, :exact_revision),
|
|
332
|
+
verification: verification.merge(base_participated: true)
|
|
333
|
+
)
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def verify_exact(output, expected, role)
|
|
337
|
+
parsed = analyze_role(:output, output)
|
|
338
|
+
{
|
|
339
|
+
output_reparsed: true,
|
|
340
|
+
byte_exact: output == expected.source,
|
|
341
|
+
semantic_match: fatal_issues(parsed).empty? && document_signature(parsed) == document_signature(expected),
|
|
342
|
+
ast_attributes_match: parsed.document_attributes == expected.document_attributes &&
|
|
343
|
+
parsed.entries.map(&:attributes) == expected.entries.map(&:attributes),
|
|
344
|
+
ordered_entries: ordered_semantics(parsed),
|
|
345
|
+
source_role: role
|
|
346
|
+
}
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def mapping_changes(before, after)
|
|
350
|
+
ordered_keys(before, after).filter_map do |key|
|
|
351
|
+
left = before.by_key[key]
|
|
352
|
+
right = after.by_key[key]
|
|
353
|
+
next if entry_state(left) == entry_state(right)
|
|
354
|
+
|
|
355
|
+
{ path: "/#{key}", ours: :unchanged, theirs: change_kind(left, right) }.freeze
|
|
356
|
+
end.freeze
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def ordered_keys(*documents)
|
|
360
|
+
documents.flat_map { |document| document.entries.map(&:key) }.uniq
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
def change_kind(before, after)
|
|
364
|
+
return :added unless before
|
|
365
|
+
return :deleted unless after
|
|
366
|
+
|
|
367
|
+
:edited
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def three_way_decisions(documents)
|
|
371
|
+
ordered_keys(documents[:ours], documents[:theirs], documents[:base]).map do |key|
|
|
372
|
+
base = documents[:base].by_key[key]
|
|
373
|
+
ours = documents[:ours].by_key[key]
|
|
374
|
+
theirs = documents[:theirs].by_key[key]
|
|
375
|
+
selected_role =
|
|
376
|
+
if entry_state(ours) == entry_state(theirs)
|
|
377
|
+
:ours
|
|
378
|
+
elsif entry_state(ours) == entry_state(base)
|
|
379
|
+
:theirs
|
|
380
|
+
elsif entry_state(theirs) == entry_state(base)
|
|
381
|
+
:ours
|
|
382
|
+
end
|
|
383
|
+
classification = {
|
|
384
|
+
path: "/#{key}",
|
|
385
|
+
ours: side_change(base, ours),
|
|
386
|
+
theirs: side_change(base, theirs)
|
|
387
|
+
}.freeze
|
|
388
|
+
Decision.new(
|
|
389
|
+
key: key,
|
|
390
|
+
base: base,
|
|
391
|
+
ours: ours,
|
|
392
|
+
theirs: theirs,
|
|
393
|
+
selected_role: selected_role,
|
|
394
|
+
classification: classification,
|
|
395
|
+
conflict: selected_role.nil?
|
|
396
|
+
)
|
|
397
|
+
end.freeze
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def side_change(base, side)
|
|
401
|
+
entry_state(base) == entry_state(side) ? :unchanged : change_kind(base, side)
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
def unmanaged_unchanged?(documents)
|
|
405
|
+
anchor_keys = documents.values.map { |document| document.by_key.keys }.reduce(&:intersection)
|
|
406
|
+
signatures = documents.transform_values { |document| unmanaged_signature(document, anchor_keys) }
|
|
407
|
+
signatures[:base] == signatures[:ours] && signatures[:base] == signatures[:theirs]
|
|
408
|
+
end
|
|
409
|
+
|
|
410
|
+
def unmanaged_signature(document, anchor_keys)
|
|
411
|
+
entries = document.entries.sort_by(&:start_line)
|
|
412
|
+
signature = []
|
|
413
|
+
cursor = 1
|
|
414
|
+
entries.each do |entry|
|
|
415
|
+
if cursor < entry.start_line
|
|
416
|
+
signature << [:source, source_lines(document.source, cursor, entry.start_line - 1)]
|
|
417
|
+
end
|
|
418
|
+
signature << [:entry, entry.key] if anchor_keys.include?(entry.key)
|
|
419
|
+
cursor = entry.end_line + 1
|
|
420
|
+
end
|
|
421
|
+
signature << [:source, source_lines(document.source, cursor, document.source.lines.length)]
|
|
422
|
+
signature
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def render_composite(request, documents, decisions)
|
|
426
|
+
by_key = decisions.to_h { |decision| [decision.key, decision] }
|
|
427
|
+
fragments = []
|
|
428
|
+
selected = []
|
|
429
|
+
cursor = 1
|
|
430
|
+
documents[:ours].entries.sort_by(&:start_line).each do |ours_entry|
|
|
431
|
+
fragments << range_fragment(:ours, cursor, ours_entry.start_line - 1) if cursor < ours_entry.start_line
|
|
432
|
+
decision = by_key.fetch(ours_entry.key)
|
|
433
|
+
selected_entry_value = decision.selected_role && decision.public_send(decision.selected_role)
|
|
434
|
+
if selected_entry_value
|
|
435
|
+
fragments << entry_fragment(decision.selected_role, selected_entry_value)
|
|
436
|
+
selected << selected_entry(selected_entry_value, decision.selected_role)
|
|
437
|
+
end
|
|
438
|
+
cursor = ours_entry.end_line + 1
|
|
439
|
+
end
|
|
440
|
+
ours_line_count = documents[:ours].source.lines.length
|
|
441
|
+
fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
|
|
442
|
+
decisions.each do |decision|
|
|
443
|
+
next if decision.ours || decision.selected_role != :theirs || !decision.theirs
|
|
444
|
+
|
|
445
|
+
fragments << entry_fragment(:theirs, decision.theirs)
|
|
446
|
+
selected << selected_entry(decision.theirs, :theirs)
|
|
447
|
+
end
|
|
448
|
+
|
|
449
|
+
rendered = render_plan(request, separated_fragments(fragments.compact, documents))
|
|
450
|
+
verification = verify_composite(rendered.content, selected)
|
|
451
|
+
unless verification[:semantic_match]
|
|
452
|
+
return verification_failure(:merge3, request, rendered, decisions, verification)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
result(
|
|
456
|
+
:merge3,
|
|
457
|
+
request,
|
|
458
|
+
output: rendered.content,
|
|
459
|
+
changes: decisions.map(&:classification),
|
|
460
|
+
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
461
|
+
verification: verification.merge(base_participated: true)
|
|
462
|
+
)
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def selected_entry(entry, role)
|
|
466
|
+
{
|
|
467
|
+
key: entry.key,
|
|
468
|
+
semantic: entry.semantic,
|
|
469
|
+
attributes: entry.attributes,
|
|
470
|
+
source_role: role,
|
|
471
|
+
source_lines: [entry.start_line, entry.end_line],
|
|
472
|
+
fragment: entry.fragment
|
|
473
|
+
}.freeze
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def verify_composite(output, selected)
|
|
477
|
+
parsed = analyze_role(:output, output)
|
|
478
|
+
expected_semantics = selected.map { |entry| [entry[:key], entry[:semantic]] }
|
|
479
|
+
expected_attributes = selected.map { |entry| [entry[:key], entry[:attributes]] }
|
|
480
|
+
actual_semantics = parsed.entries.map { |entry| [entry.key, entry.semantic] }
|
|
481
|
+
actual_attributes = parsed.entries.map { |entry| [entry.key, entry.attributes] }
|
|
482
|
+
source_match = parsed.entries.zip(selected).all? do |actual, expected|
|
|
483
|
+
actual.fragment == expected[:fragment] ||
|
|
484
|
+
(!expected[:fragment].end_with?("\n") && actual.fragment == "#{expected[:fragment]}\n")
|
|
485
|
+
end
|
|
486
|
+
{
|
|
487
|
+
output_reparsed: true,
|
|
488
|
+
semantic_match: parsed.issues.empty? && actual_semantics == expected_semantics &&
|
|
489
|
+
actual_attributes == expected_attributes && source_match,
|
|
490
|
+
ast_attributes_match: actual_attributes == expected_attributes,
|
|
491
|
+
source_match: source_match,
|
|
492
|
+
ordered_entries: actual_semantics,
|
|
493
|
+
entry_sources: selected.map { |entry| entry.slice(:key, :source_role, :source_lines) }
|
|
494
|
+
}
|
|
495
|
+
end
|
|
496
|
+
|
|
497
|
+
def ordered_semantics(document)
|
|
498
|
+
document.entries.map { |entry| [entry.key, entry.semantic] }
|
|
499
|
+
end
|
|
500
|
+
|
|
501
|
+
def document_signature(document)
|
|
502
|
+
[ordered_semantics(document), document.entries.map(&:attributes)]
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
def fatal_issues(document)
|
|
506
|
+
document.issues.select { |item| %i[parse_error structural_error].include?(item[:category]) }
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def render_conflicts(request, documents, decisions, conflicts)
|
|
510
|
+
unless conflicts.all? { |conflict| localized_conflict_position(conflict, documents) }
|
|
511
|
+
return full_file_conflict(request, documents, decisions, conflicts, :ownership_unproven)
|
|
512
|
+
end
|
|
513
|
+
unless unmanaged_unchanged?(documents)
|
|
514
|
+
return full_file_conflict(request, documents, decisions, conflicts, :unmanaged_source_change)
|
|
515
|
+
end
|
|
516
|
+
|
|
517
|
+
by_key = conflicts.to_h { |conflict| [conflict.key, conflict] }
|
|
518
|
+
inserted_conflicts = conflicts.select { |conflict| conflict.ours.nil? }
|
|
519
|
+
insert_before = inserted_conflicts.group_by do |conflict|
|
|
520
|
+
localized_conflict_position(conflict, documents)
|
|
521
|
+
end
|
|
522
|
+
fragments = []
|
|
523
|
+
cursor = 1
|
|
524
|
+
documents[:ours].entries.sort_by(&:start_line).each do |entry|
|
|
525
|
+
fragments << range_fragment(:ours, cursor, entry.start_line - 1) if cursor < entry.start_line
|
|
526
|
+
Array(insert_before[entry.key]).each { |conflict| fragments << conflict_fragment(request, conflict) }
|
|
527
|
+
conflict = by_key[entry.key]
|
|
528
|
+
fragments << (conflict ? conflict_fragment(request, conflict) : entry_fragment(:ours, entry))
|
|
529
|
+
cursor = entry.end_line + 1
|
|
530
|
+
end
|
|
531
|
+
Array(insert_before[:append]).each { |conflict| fragments << conflict_fragment(request, conflict) }
|
|
532
|
+
ours_line_count = documents[:ours].source.lines.length
|
|
533
|
+
fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
|
|
534
|
+
rendered = render_plan(request, fragments.compact)
|
|
535
|
+
conflict_failure(request, decisions, conflicts, rendered, :mapping_entry_localized_conflict)
|
|
536
|
+
end
|
|
537
|
+
|
|
538
|
+
def localized_conflict_position(conflict, documents)
|
|
539
|
+
return conflict.key if conflict.ours&.ownership_provable
|
|
540
|
+
return unless conflict.base&.ownership_provable && conflict.theirs&.ownership_provable
|
|
541
|
+
|
|
542
|
+
base_keys = documents[:base].entries.map(&:key)
|
|
543
|
+
conflict_index = base_keys.index(conflict.key)
|
|
544
|
+
return unless conflict_index
|
|
545
|
+
|
|
546
|
+
following_anchor = base_keys.drop(conflict_index + 1).find { |key| documents[:ours].by_key.key?(key) }
|
|
547
|
+
following_anchor || :append
|
|
548
|
+
end
|
|
549
|
+
|
|
550
|
+
def conflict_fragment(request, decision)
|
|
551
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
552
|
+
conflict_id: conflict_id(decision.key),
|
|
553
|
+
base: conflict_side(:base, decision.base),
|
|
554
|
+
ours: conflict_side(:ours, decision.ours),
|
|
555
|
+
theirs: conflict_side(:theirs, decision.theirs),
|
|
556
|
+
labels: request.fetch(:labels, {}),
|
|
557
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
558
|
+
metadata: { path: decision.classification[:path], category: conflict_category(decision) }
|
|
559
|
+
)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
def conflict_side(role, entry)
|
|
563
|
+
return [] unless entry
|
|
564
|
+
|
|
565
|
+
[entry_fragment(role, entry, line_bounded: true)]
|
|
566
|
+
end
|
|
567
|
+
|
|
568
|
+
def conflict_category(decision)
|
|
569
|
+
decision.ours.nil? || decision.theirs.nil? ? :delete_edit : :edit_edit
|
|
570
|
+
end
|
|
571
|
+
|
|
572
|
+
def conflict_id(key)
|
|
573
|
+
"toml-mapping-#{Digest::SHA256.hexdigest(key)[0, 16]}"
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def conflict_failure(request, decisions, conflicts, rendered, strategy)
|
|
577
|
+
failure(
|
|
578
|
+
:merge3,
|
|
579
|
+
request,
|
|
580
|
+
category: :merge_conflict,
|
|
581
|
+
message: 'TOML mapping entries changed incompatibly.',
|
|
582
|
+
changes: decisions.map(&:classification),
|
|
583
|
+
conflicts: conflicts.map { |decision| conflict_payload(decision) },
|
|
584
|
+
conflicted_output: rendered.content,
|
|
585
|
+
conflicted_source: rendered.content,
|
|
586
|
+
render_report: render_report(rendered, strategy),
|
|
587
|
+
verification: { base_participated: true }
|
|
588
|
+
)
|
|
589
|
+
end
|
|
590
|
+
|
|
591
|
+
def structural_conflict(request, documents, unsafe)
|
|
592
|
+
source_role = unsafe.first.role
|
|
593
|
+
full_file_conflict(
|
|
594
|
+
request,
|
|
595
|
+
documents,
|
|
596
|
+
safe_decisions(documents),
|
|
597
|
+
[],
|
|
598
|
+
:structural_ambiguity,
|
|
599
|
+
diagnostics: [
|
|
600
|
+
{
|
|
601
|
+
category: unsafe.first.issues.first[:category],
|
|
602
|
+
severity: :error,
|
|
603
|
+
message: "#{source_role} TOML input is unsafe for structural merge.",
|
|
604
|
+
blocking: true,
|
|
605
|
+
source_role: source_role
|
|
606
|
+
},
|
|
607
|
+
*diagnostics_for(*unsafe)
|
|
608
|
+
]
|
|
609
|
+
)
|
|
610
|
+
end
|
|
611
|
+
|
|
612
|
+
def safe_decisions(documents)
|
|
613
|
+
return [] unless documents.values.all? { |document| document.issues.empty? }
|
|
614
|
+
|
|
615
|
+
three_way_decisions(documents)
|
|
616
|
+
end
|
|
617
|
+
|
|
618
|
+
def unmanaged_conflict(request, documents)
|
|
619
|
+
full_file_conflict(
|
|
620
|
+
request,
|
|
621
|
+
documents,
|
|
622
|
+
three_way_decisions(documents),
|
|
623
|
+
[],
|
|
624
|
+
:unmanaged_source_change,
|
|
625
|
+
diagnostics: [{
|
|
626
|
+
category: :unmanaged_source_change,
|
|
627
|
+
severity: :error,
|
|
628
|
+
message: 'Unmanaged TOML layout or comments changed independently.',
|
|
629
|
+
blocking: true
|
|
630
|
+
}]
|
|
631
|
+
)
|
|
632
|
+
end
|
|
633
|
+
|
|
634
|
+
def full_file_conflict(request, documents, decisions, conflicts, reason, diagnostics: nil)
|
|
635
|
+
conflict = { conflict_id: 'toml-conflict-root', category: reason, path: '<document>' }.freeze
|
|
636
|
+
rendered = render_plan(
|
|
637
|
+
request,
|
|
638
|
+
[
|
|
639
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
640
|
+
conflict_id: conflict[:conflict_id],
|
|
641
|
+
base: conflict_source(:base, documents[:base].source),
|
|
642
|
+
ours: conflict_source(:ours, documents[:ours].source),
|
|
643
|
+
theirs: conflict_source(:theirs, documents[:theirs].source),
|
|
644
|
+
labels: request.fetch(:labels, {}),
|
|
645
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
646
|
+
metadata: { category: reason }
|
|
647
|
+
)
|
|
648
|
+
]
|
|
649
|
+
)
|
|
650
|
+
failure(
|
|
651
|
+
:merge3,
|
|
652
|
+
request,
|
|
653
|
+
category: reason,
|
|
654
|
+
message: 'TOML source cannot be safely combined structurally.',
|
|
655
|
+
diagnostics: diagnostics,
|
|
656
|
+
changes: decisions.map(&:classification),
|
|
657
|
+
conflicts: conflicts.empty? ? [conflict] : conflicts.map { |item| conflict_payload(item) },
|
|
658
|
+
fallbacks: [{ from: :mapping_entry_composite, to: :full_file_conflict, reason: reason }],
|
|
659
|
+
conflicted_output: rendered.content,
|
|
660
|
+
conflicted_source: rendered.content,
|
|
661
|
+
render_report: render_report(rendered, :full_file_conflict),
|
|
662
|
+
verification: { base_participated: true },
|
|
663
|
+
source_role: diagnostics&.first&.fetch(:source_role, nil)
|
|
664
|
+
)
|
|
665
|
+
end
|
|
666
|
+
|
|
667
|
+
def conflict_payload(decision)
|
|
668
|
+
{
|
|
669
|
+
conflict_id: conflict_id(decision.key),
|
|
670
|
+
category: conflict_category(decision),
|
|
671
|
+
path: decision.classification[:path],
|
|
672
|
+
change_classification: decision.classification
|
|
673
|
+
}.freeze
|
|
674
|
+
end
|
|
675
|
+
|
|
676
|
+
def conflict_source(role, source)
|
|
677
|
+
return [] if source.empty?
|
|
678
|
+
|
|
679
|
+
[whole_source_fragment(role, source, line_bounded: true)]
|
|
680
|
+
end
|
|
681
|
+
|
|
682
|
+
def separated_fragments(fragments, documents)
|
|
683
|
+
fragments.each_with_index.flat_map do |fragment, index|
|
|
684
|
+
next [fragment] if index == fragments.length - 1
|
|
685
|
+
next [fragment] if fragment.is_a?(Ast::Merge::SourceRender::ConflictFragment)
|
|
686
|
+
|
|
687
|
+
content = fragment_content(fragment, documents)
|
|
688
|
+
next [fragment] if content.empty? || content.end_with?("\n")
|
|
689
|
+
|
|
690
|
+
[
|
|
691
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
692
|
+
content: "#{content}\n",
|
|
693
|
+
reason: :mapping_entry_separator,
|
|
694
|
+
producer: provider_id,
|
|
695
|
+
metadata: fragment.metadata.merge(copied_source: true)
|
|
696
|
+
)
|
|
697
|
+
]
|
|
698
|
+
end
|
|
699
|
+
end
|
|
700
|
+
|
|
701
|
+
def fragment_content(fragment, documents)
|
|
702
|
+
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
703
|
+
|
|
704
|
+
document = documents.fetch(fragment.revision)
|
|
705
|
+
source_lines(document.source, fragment.start_line, fragment.end_line)
|
|
706
|
+
end
|
|
707
|
+
|
|
708
|
+
def whole_source_fragment(role, source, line_bounded: false)
|
|
709
|
+
if source.empty?
|
|
710
|
+
return Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
711
|
+
content: '',
|
|
712
|
+
reason: :exact_empty_source,
|
|
713
|
+
producer: provider_id,
|
|
714
|
+
metadata: { source_role: role }
|
|
715
|
+
)
|
|
716
|
+
end
|
|
717
|
+
fragment = range_fragment(role, 1, source.lines.length)
|
|
718
|
+
return fragment unless line_bounded && !source.end_with?("\n")
|
|
719
|
+
|
|
720
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
721
|
+
content: "#{source}\n",
|
|
722
|
+
reason: :conflict_line_boundary,
|
|
723
|
+
producer: provider_id,
|
|
724
|
+
metadata: { source_role: role, copied_source: true }
|
|
725
|
+
)
|
|
726
|
+
end
|
|
727
|
+
|
|
728
|
+
def entry_fragment(role, entry, line_bounded: false)
|
|
729
|
+
unless line_bounded && !entry.fragment.end_with?("\n")
|
|
730
|
+
return range_fragment(role, entry.start_line, entry.end_line, toml_key: entry.key)
|
|
731
|
+
end
|
|
732
|
+
|
|
733
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
734
|
+
content: "#{entry.fragment}\n",
|
|
735
|
+
reason: :conflict_line_boundary,
|
|
736
|
+
producer: provider_id,
|
|
737
|
+
metadata: { source_role: role, toml_key: entry.key, copied_source: true }
|
|
738
|
+
)
|
|
739
|
+
end
|
|
740
|
+
|
|
741
|
+
def range_fragment(role, start_line, end_line, **metadata)
|
|
742
|
+
return if end_line < start_line
|
|
743
|
+
|
|
744
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
745
|
+
revision: role,
|
|
746
|
+
start_line: start_line,
|
|
747
|
+
end_line: end_line,
|
|
748
|
+
metadata: { source_role: role }.merge(metadata)
|
|
749
|
+
)
|
|
750
|
+
end
|
|
751
|
+
|
|
752
|
+
def source_lines(source, start_line, end_line)
|
|
753
|
+
return '' if end_line < start_line
|
|
754
|
+
|
|
755
|
+
source.lines.slice(start_line - 1, end_line - start_line + 1).to_a.join
|
|
756
|
+
end
|
|
757
|
+
|
|
758
|
+
def render_plan(request, fragments)
|
|
759
|
+
Ast::Merge::SourceRender::Renderer.new.render(
|
|
760
|
+
Ast::Merge::SourceRender::Plan.new(
|
|
761
|
+
sources: {
|
|
762
|
+
base: request[:base_source].to_s,
|
|
763
|
+
ours: request[:ours_source] || request[:current_source].to_s,
|
|
764
|
+
theirs: request[:theirs_source] || request[:incoming_source].to_s
|
|
765
|
+
},
|
|
766
|
+
fragments: fragments,
|
|
767
|
+
metadata: { provider_id: provider_id }
|
|
768
|
+
)
|
|
769
|
+
)
|
|
770
|
+
end
|
|
771
|
+
|
|
772
|
+
def render_report(rendered, strategy)
|
|
773
|
+
{
|
|
774
|
+
strategy: strategy,
|
|
775
|
+
line_records: rendered.line_records,
|
|
776
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
777
|
+
conflicts: rendered.conflicts,
|
|
778
|
+
verification_input: rendered.verification_input
|
|
779
|
+
}
|
|
780
|
+
end
|
|
781
|
+
|
|
782
|
+
def issue(category, message, **metadata)
|
|
783
|
+
{ category: category, message: message, **metadata }.freeze
|
|
784
|
+
end
|
|
785
|
+
|
|
786
|
+
def diagnostics_for(*documents)
|
|
787
|
+
documents.flat_map do |document|
|
|
788
|
+
document.issues.map do |item|
|
|
789
|
+
item.merge(severity: :warning, blocking: false, source_role: document.role).freeze
|
|
790
|
+
end
|
|
791
|
+
end.freeze
|
|
792
|
+
end
|
|
793
|
+
|
|
794
|
+
def invalid_document_failure(operation, request, documents)
|
|
795
|
+
first = documents.first
|
|
796
|
+
failure(
|
|
797
|
+
operation,
|
|
798
|
+
request,
|
|
799
|
+
category: first.issues.first[:category],
|
|
800
|
+
message: "#{first.role} TOML input is unsafe",
|
|
801
|
+
diagnostics: diagnostics_for(*documents).map { |diagnostic| diagnostic.merge(blocking: true) },
|
|
802
|
+
source_role: first.role
|
|
803
|
+
)
|
|
804
|
+
end
|
|
805
|
+
|
|
806
|
+
def verification_failure(operation, request, rendered, decisions, verification)
|
|
807
|
+
failure(
|
|
808
|
+
operation,
|
|
809
|
+
request,
|
|
810
|
+
category: :verification_failure,
|
|
811
|
+
message: 'Rendered TOML did not preserve planned ordered semantics, AST attributes, and source fragments.',
|
|
812
|
+
changes: decisions.map(&:classification),
|
|
813
|
+
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
814
|
+
verification: operation == :merge3 ? verification.merge(base_participated: true) : verification
|
|
815
|
+
)
|
|
816
|
+
end
|
|
817
|
+
|
|
818
|
+
def result(operation, request, diagnostics: [], changes: [], conflicts: [], fallbacks: [],
|
|
819
|
+
render_report: {}, verification: {}, **payload)
|
|
820
|
+
Ast::Merge::ProviderResult.build(
|
|
821
|
+
operation: operation,
|
|
822
|
+
success: true,
|
|
823
|
+
envelope: envelope(
|
|
824
|
+
request,
|
|
825
|
+
diagnostics: diagnostics,
|
|
826
|
+
changes: changes,
|
|
827
|
+
conflicts: conflicts,
|
|
828
|
+
fallbacks: fallbacks,
|
|
829
|
+
render_report: render_report,
|
|
830
|
+
verification: verification
|
|
831
|
+
),
|
|
832
|
+
**payload
|
|
833
|
+
)
|
|
834
|
+
end
|
|
835
|
+
|
|
836
|
+
def failure(operation, request, category:, message:, diagnostics: nil, changes: [], conflicts: [],
|
|
837
|
+
fallbacks: [], render_report: {}, verification: {}, **payload)
|
|
838
|
+
diagnostics ||= [{ category: category, severity: :error, message: message, blocking: true }]
|
|
839
|
+
Ast::Merge::ProviderResult.build(
|
|
840
|
+
operation: operation,
|
|
841
|
+
success: false,
|
|
842
|
+
envelope: envelope(
|
|
843
|
+
request,
|
|
844
|
+
diagnostics: diagnostics,
|
|
845
|
+
changes: changes,
|
|
846
|
+
conflicts: conflicts,
|
|
847
|
+
fallbacks: fallbacks,
|
|
848
|
+
render_report: render_report,
|
|
849
|
+
verification: verification
|
|
850
|
+
),
|
|
851
|
+
**payload
|
|
852
|
+
)
|
|
853
|
+
end
|
|
854
|
+
|
|
855
|
+
def envelope(request, **fields)
|
|
856
|
+
{
|
|
857
|
+
provider: {
|
|
858
|
+
provider_id: provider_id,
|
|
859
|
+
family: family,
|
|
860
|
+
dialect: request[:dialect] || :toml,
|
|
861
|
+
backend: request[:backend] || backend_id.to_sym,
|
|
862
|
+
package: package_name,
|
|
863
|
+
package_version: package_version
|
|
864
|
+
},
|
|
865
|
+
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
866
|
+
}.merge(fields)
|
|
867
|
+
end
|
|
868
|
+
end
|
|
869
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
870
|
+
end
|
|
871
|
+
end
|
data/lib/toml/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Toml
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.3'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/toml/merge.rb
CHANGED
|
@@ -108,6 +108,7 @@ module Toml
|
|
|
108
108
|
autoload :NodeWrapper, 'toml/merge/node_wrapper'
|
|
109
109
|
autoload :ConflictResolver, 'toml/merge/conflict_resolver'
|
|
110
110
|
autoload :SmartMerger, 'toml/merge/smart_merger'
|
|
111
|
+
autoload :SourcePreservingProvider, 'toml/merge/source_preserving_provider'
|
|
111
112
|
autoload :TableMatchRefiner, 'toml/merge/table_match_refiner'
|
|
112
113
|
|
|
113
114
|
class << self
|
|
@@ -365,6 +366,8 @@ module Toml
|
|
|
365
366
|
end
|
|
366
367
|
|
|
367
368
|
Toml::Merge.register_backend!
|
|
369
|
+
require_relative 'merge/provider'
|
|
370
|
+
Toml::Merge.register_provider!
|
|
368
371
|
|
|
369
372
|
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
370
373
|
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
data/sig/toml/merge.rbs
CHANGED
|
@@ -1,5 +1,26 @@
|
|
|
1
1
|
module Toml
|
|
2
2
|
module Merge
|
|
3
|
+
class SourcePreservingProvider
|
|
4
|
+
def provider_id: () -> String
|
|
5
|
+
def family: () -> String
|
|
6
|
+
def backend_reference: () -> untyped
|
|
7
|
+
def backend_id: () -> String
|
|
8
|
+
def package_name: () -> String
|
|
9
|
+
def package_version: () -> String
|
|
10
|
+
def role: () -> Symbol
|
|
11
|
+
def capabilities: () -> Hash[Symbol, untyped]
|
|
12
|
+
def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
13
|
+
def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
14
|
+
def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
15
|
+
def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
class Provider < SourcePreservingProvider
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.merge_provider: () -> Provider
|
|
22
|
+
def self.register_provider!: (?replace: bool) -> untyped
|
|
23
|
+
|
|
3
24
|
module Version
|
|
4
25
|
VERSION: String
|
|
5
26
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: toml-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.3
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Peter H. Boling
|
|
@@ -43,28 +43,28 @@ dependencies:
|
|
|
43
43
|
requirements:
|
|
44
44
|
- - '='
|
|
45
45
|
- !ruby/object:Gem::Version
|
|
46
|
-
version: 7.1.
|
|
46
|
+
version: 7.1.3
|
|
47
47
|
type: :runtime
|
|
48
48
|
prerelease: false
|
|
49
49
|
version_requirements: !ruby/object:Gem::Requirement
|
|
50
50
|
requirements:
|
|
51
51
|
- - '='
|
|
52
52
|
- !ruby/object:Gem::Version
|
|
53
|
-
version: 7.1.
|
|
53
|
+
version: 7.1.3
|
|
54
54
|
- !ruby/object:Gem::Dependency
|
|
55
55
|
name: tree_haver
|
|
56
56
|
requirement: !ruby/object:Gem::Requirement
|
|
57
57
|
requirements:
|
|
58
58
|
- - '='
|
|
59
59
|
- !ruby/object:Gem::Version
|
|
60
|
-
version: 7.1.
|
|
60
|
+
version: 7.1.3
|
|
61
61
|
type: :runtime
|
|
62
62
|
prerelease: false
|
|
63
63
|
version_requirements: !ruby/object:Gem::Requirement
|
|
64
64
|
requirements:
|
|
65
65
|
- - '='
|
|
66
66
|
- !ruby/object:Gem::Version
|
|
67
|
-
version: 7.1.
|
|
67
|
+
version: 7.1.3
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: version_gem
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,20 +91,20 @@ dependencies:
|
|
|
91
91
|
requirements:
|
|
92
92
|
- - "~>"
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: '
|
|
94
|
+
version: '3.0'
|
|
95
95
|
- - ">="
|
|
96
96
|
- !ruby/object:Gem::Version
|
|
97
|
-
version:
|
|
97
|
+
version: 3.0.0
|
|
98
98
|
type: :development
|
|
99
99
|
prerelease: false
|
|
100
100
|
version_requirements: !ruby/object:Gem::Requirement
|
|
101
101
|
requirements:
|
|
102
102
|
- - "~>"
|
|
103
103
|
- !ruby/object:Gem::Version
|
|
104
|
-
version: '
|
|
104
|
+
version: '3.0'
|
|
105
105
|
- - ">="
|
|
106
106
|
- !ruby/object:Gem::Version
|
|
107
|
-
version:
|
|
107
|
+
version: 3.0.0
|
|
108
108
|
- !ruby/object:Gem::Dependency
|
|
109
109
|
name: bundler-audit
|
|
110
110
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -182,7 +182,7 @@ dependencies:
|
|
|
182
182
|
version: '3.2'
|
|
183
183
|
- - ">="
|
|
184
184
|
- !ruby/object:Gem::Version
|
|
185
|
-
version: 3.2.
|
|
185
|
+
version: 3.2.2
|
|
186
186
|
type: :development
|
|
187
187
|
prerelease: false
|
|
188
188
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -192,7 +192,7 @@ dependencies:
|
|
|
192
192
|
version: '3.2'
|
|
193
193
|
- - ">="
|
|
194
194
|
- !ruby/object:Gem::Version
|
|
195
|
-
version: 3.2.
|
|
195
|
+
version: 3.2.2
|
|
196
196
|
- !ruby/object:Gem::Dependency
|
|
197
197
|
name: kettle-test
|
|
198
198
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -222,7 +222,7 @@ dependencies:
|
|
|
222
222
|
version: '3.2'
|
|
223
223
|
- - ">="
|
|
224
224
|
- !ruby/object:Gem::Version
|
|
225
|
-
version: 3.2.
|
|
225
|
+
version: 3.2.5
|
|
226
226
|
type: :development
|
|
227
227
|
prerelease: false
|
|
228
228
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -232,7 +232,7 @@ dependencies:
|
|
|
232
232
|
version: '3.2'
|
|
233
233
|
- - ">="
|
|
234
234
|
- !ruby/object:Gem::Version
|
|
235
|
-
version: 3.2.
|
|
235
|
+
version: 3.2.5
|
|
236
236
|
- !ruby/object:Gem::Dependency
|
|
237
237
|
name: ruby-progressbar
|
|
238
238
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -308,7 +308,9 @@ files:
|
|
|
308
308
|
- lib/toml/merge/merge_result.rb
|
|
309
309
|
- lib/toml/merge/node_type_normalizer.rb
|
|
310
310
|
- lib/toml/merge/node_wrapper.rb
|
|
311
|
+
- lib/toml/merge/provider.rb
|
|
311
312
|
- lib/toml/merge/smart_merger.rb
|
|
313
|
+
- lib/toml/merge/source_preserving_provider.rb
|
|
312
314
|
- lib/toml/merge/table_match_refiner.rb
|
|
313
315
|
- lib/toml/merge/version.rb
|
|
314
316
|
- sig/toml/merge.rbs
|
|
@@ -318,10 +320,10 @@ licenses:
|
|
|
318
320
|
- PolyForm-Small-Business-1.0.0
|
|
319
321
|
metadata:
|
|
320
322
|
homepage_uri: https://structuredmerge.org
|
|
321
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
322
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
323
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.3
|
|
324
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.3/CHANGELOG.md
|
|
323
325
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
324
|
-
documentation_uri: https://www.rubydoc.info/gems/toml-merge/7.1.
|
|
326
|
+
documentation_uri: https://www.rubydoc.info/gems/toml-merge/7.1.3
|
|
325
327
|
funding_uri: https://github.com/sponsors/pboling
|
|
326
328
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
327
329
|
news_uri: https://www.railsbling.com/tags/toml-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|