psych-merge 7.1.7 → 7.1.8
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 +3 -4
- data/lib/psych/merge/native_projection.rb +49 -0
- data/lib/psych/merge/provider.rb +16 -733
- data/lib/psych/merge/provider_extension.rb +29 -0
- data/lib/psych/merge/rspec/provider_snapshot_extension.rb +18 -0
- data/lib/psych/merge/version.rb +1 -1
- data/lib/psych/merge.rb +2 -0
- data.tar.gz.sig +0 -0
- metadata +19 -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: 8b38c9a6f3274ff8bb9d92147d02ddf81c6d4d02c4b36c534ee826ce73779d4d
|
|
4
|
+
data.tar.gz: 63f51d481149fb7cc7221b183333ee1d13a5a7b1250ac8928de741719cea9506
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ff82e5d7c19a232ad13d380d9b4cf86fc86fe5da2307e9f0cf1f6366f8cb7f51e00b33f728ac489c00c5633f73ea46d58a3521bc657b00d4e2a964bceb86c5be
|
|
7
|
+
data.tar.gz: e1156c490c572dc4f53d2eac908603e3f768468a0bb796e85e783b17922a7f361053301e3c40ea02462e46cdc0caca33acad57c3602b53f330e61c8fc103db4e
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -26,9 +26,8 @@ AST analysis. It is built on [ast-merge][ast-merge] and [tree_haver][tree_haver]
|
|
|
26
26
|
and shares YAML-family behavior with [yaml-merge][yaml-merge].
|
|
27
27
|
|
|
28
28
|
`psych-merge` is a YAML provider gem, not an alternate home for YAML merge
|
|
29
|
-
semantics.
|
|
30
|
-
|
|
31
|
-
source-preserving edit plans supplied by the StructuredMerge stack. Partial YAML
|
|
29
|
+
semantics. Its provider subclasses `Yaml::Merge::SourcePreservingProvider` and
|
|
30
|
+
supplies only Psych AST validation and projection. Partial YAML
|
|
32
31
|
insertion, replacement, and removal should route through `ast-crispr` rather
|
|
33
32
|
than converting documents to Ruby objects and serializing them with `Psych.dump`.
|
|
34
33
|
|
|
@@ -36,7 +35,7 @@ than converting documents to Ruby objects and serializing them with `Psych.dump`
|
|
|
36
35
|
|
|
37
36
|
- **Psych-Powered**: Uses Ruby's built-in Psych parser for YAML AST analysis
|
|
38
37
|
- **YAML-Aware**: Understands YAML structure including mappings, sequences, and scalars
|
|
39
|
-
- **YAML Family Behavior**:
|
|
38
|
+
- **YAML Family Behavior**: Inherits shared YAML merge semantics from `yaml-merge`
|
|
40
39
|
- **Intelligent**: Matches nodes by structural signatures
|
|
41
40
|
- **Fuzzy Key Matching**: `MappingMatchRefiner` matches similar keys (e.g., `database_url` ↔ `db_url`)
|
|
42
41
|
using Levenshtein distance for typos and naming convention differences
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Psych
|
|
4
|
+
module Merge
|
|
5
|
+
# Shared native Psych projection used by semantic verification and contract
|
|
6
|
+
# snapshot extensions. Location and ownership remain separate concerns.
|
|
7
|
+
module NativeProjection
|
|
8
|
+
ATTRIBUTE_NAMES = %i[anchor tag style plain quoted implicit implicit_end version tag_directives].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def semantic(node)
|
|
13
|
+
{
|
|
14
|
+
type: node.class.name.delete_prefix('Psych::Nodes::'),
|
|
15
|
+
value: (node.value if node.respond_to?(:value)),
|
|
16
|
+
tag: (node.tag if node.respond_to?(:tag)),
|
|
17
|
+
children: children(node).map { |child| semantic(child) }
|
|
18
|
+
}.compact.freeze
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def attribute_tree(node)
|
|
22
|
+
{
|
|
23
|
+
attributes: attributes(node),
|
|
24
|
+
children: children(node).map { |child| attribute_tree(child) }
|
|
25
|
+
}.freeze
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def attributes(node)
|
|
29
|
+
ATTRIBUTE_NAMES.each_with_object({}) do |name, result|
|
|
30
|
+
result[name] = public_value(node.public_send(name)) if node.respond_to?(name)
|
|
31
|
+
end.freeze
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def public_value(value)
|
|
35
|
+
case value
|
|
36
|
+
when Array then value.map { |item| public_value(item) }
|
|
37
|
+
when Hash then value.to_h { |key, item| [key.to_s, public_value(item)] }
|
|
38
|
+
when String, Integer, Float, TrueClass, FalseClass, NilClass then value
|
|
39
|
+
else value.to_s
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def children(node)
|
|
44
|
+
Array(node.respond_to?(:children) ? node.children : [])
|
|
45
|
+
end
|
|
46
|
+
private_class_method :children
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
end
|
data/lib/psych/merge/provider.rb
CHANGED
|
@@ -1,134 +1,16 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require 'digest'
|
|
4
|
-
|
|
5
3
|
module Psych
|
|
6
|
-
# Psych-backed YAML
|
|
4
|
+
# Psych-backed YAML provider integration.
|
|
7
5
|
module Merge
|
|
8
|
-
#
|
|
9
|
-
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity --
|
|
10
|
-
class Provider
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
:start_line,
|
|
17
|
-
:end_line,
|
|
18
|
-
:fragment,
|
|
19
|
-
:semantic,
|
|
20
|
-
:attributes,
|
|
21
|
-
:role,
|
|
22
|
-
:ownership_provable
|
|
23
|
-
)
|
|
24
|
-
Document = Data.define(
|
|
25
|
-
:role,
|
|
26
|
-
:source,
|
|
27
|
-
:analysis,
|
|
28
|
-
:entries,
|
|
29
|
-
:by_key,
|
|
30
|
-
:issues,
|
|
31
|
-
:document_attributes
|
|
32
|
-
)
|
|
33
|
-
Decision = Data.define(:key, :base, :ours, :theirs, :selected_role, :classification, :conflict)
|
|
34
|
-
|
|
35
|
-
def provider_id = 'ruby.yaml.psych'
|
|
36
|
-
def family = 'yaml'
|
|
37
|
-
|
|
38
|
-
def capabilities
|
|
39
|
-
{
|
|
40
|
-
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
41
|
-
dialects: DIALECTS,
|
|
42
|
-
backends: %i[psych],
|
|
43
|
-
profiles: [DEFAULT_PROFILE],
|
|
44
|
-
role: :backend,
|
|
45
|
-
source_preservation: %i[
|
|
46
|
-
exact_source
|
|
47
|
-
mapping_entry_fragments
|
|
48
|
-
comment_ownership
|
|
49
|
-
line_provenance
|
|
50
|
-
reparse
|
|
51
|
-
semantic_verification
|
|
52
|
-
ast_attribute_verification
|
|
53
|
-
]
|
|
54
|
-
}.freeze
|
|
55
|
-
end
|
|
56
|
-
|
|
57
|
-
def analyze(request)
|
|
58
|
-
document = analyze_role(:source, request.fetch(:source))
|
|
59
|
-
return invalid_document_failure(:analyze, request, [document]) unless document.issues.empty?
|
|
60
|
-
|
|
61
|
-
result(
|
|
62
|
-
:analyze,
|
|
63
|
-
request,
|
|
64
|
-
analysis: analysis_payload(document),
|
|
65
|
-
verification: { source_parsed: true, structurally_unambiguous: true }
|
|
66
|
-
)
|
|
67
|
-
end
|
|
68
|
-
|
|
69
|
-
def diff2(request)
|
|
70
|
-
before = analyze_role(:before, request.fetch(:before_source))
|
|
71
|
-
after = analyze_role(:after, request.fetch(:after_source))
|
|
72
|
-
unsafe = [before, after].reject { |document| document.issues.empty? }
|
|
73
|
-
return invalid_document_failure(:diff2, request, unsafe) unless unsafe.empty?
|
|
74
|
-
|
|
75
|
-
changes = mapping_changes(before, after)
|
|
76
|
-
result(
|
|
77
|
-
:diff2,
|
|
78
|
-
request,
|
|
79
|
-
diff: { before: analysis_payload(before), after: analysis_payload(after), changes: changes },
|
|
80
|
-
changes: changes,
|
|
81
|
-
verification: { before_parsed: true, after_parsed: true }
|
|
82
|
-
)
|
|
83
|
-
end
|
|
84
|
-
|
|
85
|
-
def merge2(request)
|
|
86
|
-
documents = {
|
|
87
|
-
incoming: analyze_role(:incoming, request.fetch(:incoming_source)),
|
|
88
|
-
current: analyze_role(:current, request.fetch(:current_source))
|
|
89
|
-
}
|
|
90
|
-
unsafe = documents.values.reject { |document| document.issues.empty? }
|
|
91
|
-
return invalid_document_failure(:merge2, request, unsafe) unless unsafe.empty?
|
|
92
|
-
|
|
93
|
-
additions = documents[:incoming].entries.reject { |entry| documents[:current].by_key.key?(entry.key) }
|
|
94
|
-
render_documents = { ours: documents[:current], theirs: documents[:incoming] }
|
|
95
|
-
fragments = [whole_source_fragment(:ours, documents[:current].source)]
|
|
96
|
-
fragments.concat(additions.map { |entry| entry_fragment(:theirs, entry) })
|
|
97
|
-
rendered = render_plan(request, separated_fragments(fragments, render_documents))
|
|
98
|
-
expected = [
|
|
99
|
-
*documents[:current].entries.map { |entry| selected_entry(entry, :current) },
|
|
100
|
-
*additions.map { |entry| selected_entry(entry, :incoming) }
|
|
101
|
-
]
|
|
102
|
-
verification = verify_composite(rendered.content, expected)
|
|
103
|
-
return verification_failure(:merge2, request, rendered, [], verification) unless verification[:semantic_match]
|
|
104
|
-
|
|
105
|
-
result(
|
|
106
|
-
:merge2,
|
|
107
|
-
request,
|
|
108
|
-
output: rendered.content,
|
|
109
|
-
changes: mapping_changes(documents[:current], analyze_role(:output, rendered.content)),
|
|
110
|
-
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
111
|
-
verification: verification
|
|
112
|
-
)
|
|
113
|
-
end
|
|
114
|
-
|
|
115
|
-
def merge3(request)
|
|
116
|
-
documents = %i[base ours theirs].to_h do |role|
|
|
117
|
-
[role, analyze_role(role, request.fetch(:"#{role}_source"))]
|
|
118
|
-
end
|
|
119
|
-
exact_role = exact_revision_role(documents)
|
|
120
|
-
return render_exact(request, documents, exact_role) if exact_role
|
|
121
|
-
|
|
122
|
-
unsafe = documents.values.reject { |document| document.issues.empty? }
|
|
123
|
-
return structural_conflict(request, documents, unsafe) unless unsafe.empty?
|
|
124
|
-
return unmanaged_conflict(request, documents) unless unmanaged_unchanged?(documents)
|
|
125
|
-
|
|
126
|
-
decisions = three_way_decisions(documents)
|
|
127
|
-
conflicts = decisions.select(&:conflict)
|
|
128
|
-
return render_conflicts(request, documents, decisions, conflicts) unless conflicts.empty?
|
|
129
|
-
|
|
130
|
-
render_composite(request, documents, decisions)
|
|
131
|
-
end
|
|
6
|
+
# Native Psych projection for the shared YAML source-preserving provider.
|
|
7
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- native AST validation and projection remain one adapter boundary
|
|
8
|
+
class Provider < ::Yaml::Merge::SourcePreservingProvider
|
|
9
|
+
PROVIDER_ID = 'ruby.yaml.psych'
|
|
10
|
+
BACKEND = BACKEND_REFERENCE
|
|
11
|
+
PACKAGE_NAME = Merge::PACKAGE_NAME
|
|
12
|
+
PACKAGE_VERSION = Version::VERSION
|
|
13
|
+
ROLE = :backend
|
|
132
14
|
|
|
133
15
|
private
|
|
134
16
|
|
|
@@ -165,16 +47,8 @@ module Psych
|
|
|
165
47
|
invalid_document(role, source, :structural_error, e.message)
|
|
166
48
|
end
|
|
167
49
|
|
|
168
|
-
def
|
|
169
|
-
|
|
170
|
-
role: role,
|
|
171
|
-
source: source,
|
|
172
|
-
analysis: nil,
|
|
173
|
-
entries: [].freeze,
|
|
174
|
-
by_key: {}.freeze,
|
|
175
|
-
issues: [issue(category, message)].freeze,
|
|
176
|
-
document_attributes: {}
|
|
177
|
-
)
|
|
50
|
+
def analysis_extensions(document)
|
|
51
|
+
[ProviderExtension.call(analysis: document.analysis)]
|
|
178
52
|
end
|
|
179
53
|
|
|
180
54
|
def document_issues(document)
|
|
@@ -253,14 +127,13 @@ module Psych
|
|
|
253
127
|
key_line = key.start_line || 1
|
|
254
128
|
value_end = [value.end_line || key.end_line || key_line, key_line].max
|
|
255
129
|
leading_start = owned_leading_start(analysis, mapping, index, key_line)
|
|
256
|
-
fragment = source_lines(source, leading_start, value_end)
|
|
257
130
|
Entry.new(
|
|
258
131
|
key: key.node.value.to_s,
|
|
259
132
|
start_line: leading_start,
|
|
260
133
|
end_line: value_end,
|
|
261
|
-
fragment:
|
|
262
|
-
semantic:
|
|
263
|
-
attributes:
|
|
134
|
+
fragment: source_lines(source, leading_start, value_end),
|
|
135
|
+
semantic: NativeProjection.semantic(value.node),
|
|
136
|
+
attributes: NativeProjection.attribute_tree(value.node),
|
|
264
137
|
role: role,
|
|
265
138
|
ownership_provable: ownership_provable?(analysis, mapping, index, key_line, leading_start)
|
|
266
139
|
)
|
|
@@ -285,603 +158,13 @@ module Psych
|
|
|
285
158
|
end
|
|
286
159
|
|
|
287
160
|
def semantic_key(node)
|
|
288
|
-
return [:complex,
|
|
161
|
+
return [:complex, NativeProjection.semantic(node)] unless node.is_a?(::Psych::Nodes::Scalar)
|
|
289
162
|
|
|
290
163
|
[node.value, node.tag]
|
|
291
164
|
end
|
|
292
165
|
|
|
293
|
-
def ast_semantic(node)
|
|
294
|
-
{
|
|
295
|
-
type: node.class.name.delete_prefix('Psych::Nodes::'),
|
|
296
|
-
value: (node.value if node.respond_to?(:value)),
|
|
297
|
-
tag: (node.tag if node.respond_to?(:tag)),
|
|
298
|
-
children: Array(node.respond_to?(:children) ? node.children : []).map { |child| ast_semantic(child) }
|
|
299
|
-
}.compact.freeze
|
|
300
|
-
end
|
|
301
|
-
|
|
302
|
-
def ast_attribute_tree(node)
|
|
303
|
-
{
|
|
304
|
-
attributes: ast_attributes(node),
|
|
305
|
-
children: Array(node.respond_to?(:children) ? node.children : []).map { |child| ast_attribute_tree(child) }
|
|
306
|
-
}.freeze
|
|
307
|
-
end
|
|
308
|
-
|
|
309
166
|
def ast_attributes(node)
|
|
310
|
-
|
|
311
|
-
attribute_names.each_with_object({}) do |name, attrs|
|
|
312
|
-
attrs[name] = public_value(node.public_send(name)) if node.respond_to?(name)
|
|
313
|
-
end.freeze
|
|
314
|
-
end
|
|
315
|
-
|
|
316
|
-
def public_value(value)
|
|
317
|
-
case value
|
|
318
|
-
when Array then value.map { |item| public_value(item) }
|
|
319
|
-
when Hash then value.to_h { |key, item| [key.to_s, public_value(item)] }
|
|
320
|
-
when String, Integer, Float, TrueClass, FalseClass, NilClass then value
|
|
321
|
-
else value.to_s
|
|
322
|
-
end
|
|
323
|
-
end
|
|
324
|
-
|
|
325
|
-
def analysis_payload(document)
|
|
326
|
-
{
|
|
327
|
-
dialect: 'yaml',
|
|
328
|
-
backend: 'psych',
|
|
329
|
-
document_attributes: document.document_attributes,
|
|
330
|
-
entries: document.entries.map do |entry|
|
|
331
|
-
{
|
|
332
|
-
key: entry.key,
|
|
333
|
-
value: entry.semantic,
|
|
334
|
-
attributes: entry.attributes,
|
|
335
|
-
source_lines: [entry.start_line, entry.end_line]
|
|
336
|
-
}
|
|
337
|
-
end,
|
|
338
|
-
structurally_unambiguous: document.issues.empty?
|
|
339
|
-
}
|
|
340
|
-
end
|
|
341
|
-
|
|
342
|
-
def entry_state(entry)
|
|
343
|
-
entry && [entry.semantic, entry.attributes, entry.fragment]
|
|
344
|
-
end
|
|
345
|
-
|
|
346
|
-
def exact_revision_role(documents)
|
|
347
|
-
return :ours if documents[:ours].source == documents[:theirs].source
|
|
348
|
-
return :ours if documents[:base].source == documents[:theirs].source
|
|
349
|
-
|
|
350
|
-
:theirs if documents[:base].source == documents[:ours].source
|
|
351
|
-
end
|
|
352
|
-
|
|
353
|
-
def render_exact(request, documents, role)
|
|
354
|
-
winner = documents.fetch(role)
|
|
355
|
-
return structural_conflict(request, documents, [winner]) unless fatal_issues(winner).empty?
|
|
356
|
-
|
|
357
|
-
rendered = render_plan(request, [whole_source_fragment(role, winner.source)])
|
|
358
|
-
verification = verify_exact(rendered.content, winner, role)
|
|
359
|
-
return verification_failure(:merge3, request, rendered, [], verification) unless verification[:semantic_match]
|
|
360
|
-
|
|
361
|
-
result(
|
|
362
|
-
:merge3,
|
|
363
|
-
request,
|
|
364
|
-
output: rendered.content,
|
|
365
|
-
changes: mapping_changes(documents[:base], winner),
|
|
366
|
-
diagnostics: diagnostics_for(*documents.values),
|
|
367
|
-
render_report: render_report(rendered, :exact_revision),
|
|
368
|
-
verification: verification.merge(base_participated: true)
|
|
369
|
-
)
|
|
370
|
-
end
|
|
371
|
-
|
|
372
|
-
def verify_exact(output, expected, role)
|
|
373
|
-
parsed = analyze_role(:output, output)
|
|
374
|
-
{
|
|
375
|
-
output_reparsed: true,
|
|
376
|
-
byte_exact: output == expected.source,
|
|
377
|
-
semantic_match: fatal_issues(parsed).empty? && document_signature(parsed) == document_signature(expected),
|
|
378
|
-
ast_attributes_match: parsed.document_attributes == expected.document_attributes &&
|
|
379
|
-
parsed.entries.map(&:attributes) == expected.entries.map(&:attributes),
|
|
380
|
-
ordered_entries: ordered_semantics(parsed),
|
|
381
|
-
source_role: role
|
|
382
|
-
}
|
|
383
|
-
end
|
|
384
|
-
|
|
385
|
-
def mapping_changes(before, after)
|
|
386
|
-
ordered_keys(before, after).filter_map do |key|
|
|
387
|
-
left = before.by_key[key]
|
|
388
|
-
right = after.by_key[key]
|
|
389
|
-
next if entry_state(left) == entry_state(right)
|
|
390
|
-
|
|
391
|
-
{ path: "/#{key}", ours: :unchanged, theirs: change_kind(left, right) }.freeze
|
|
392
|
-
end.freeze
|
|
393
|
-
end
|
|
394
|
-
|
|
395
|
-
def ordered_keys(*documents)
|
|
396
|
-
documents.flat_map { |document| document.entries.map(&:key) }.uniq
|
|
397
|
-
end
|
|
398
|
-
|
|
399
|
-
def change_kind(before, after)
|
|
400
|
-
return :added unless before
|
|
401
|
-
return :deleted unless after
|
|
402
|
-
|
|
403
|
-
:edited
|
|
404
|
-
end
|
|
405
|
-
|
|
406
|
-
def three_way_decisions(documents)
|
|
407
|
-
ordered_keys(documents[:ours], documents[:theirs], documents[:base]).map do |key|
|
|
408
|
-
base = documents[:base].by_key[key]
|
|
409
|
-
ours = documents[:ours].by_key[key]
|
|
410
|
-
theirs = documents[:theirs].by_key[key]
|
|
411
|
-
selected_role =
|
|
412
|
-
if entry_state(ours) == entry_state(theirs)
|
|
413
|
-
:ours
|
|
414
|
-
elsif entry_state(ours) == entry_state(base)
|
|
415
|
-
:theirs
|
|
416
|
-
elsif entry_state(theirs) == entry_state(base)
|
|
417
|
-
:ours
|
|
418
|
-
end
|
|
419
|
-
classification = {
|
|
420
|
-
path: "/#{key}",
|
|
421
|
-
ours: side_change(base, ours),
|
|
422
|
-
theirs: side_change(base, theirs)
|
|
423
|
-
}.freeze
|
|
424
|
-
Decision.new(
|
|
425
|
-
key: key,
|
|
426
|
-
base: base,
|
|
427
|
-
ours: ours,
|
|
428
|
-
theirs: theirs,
|
|
429
|
-
selected_role: selected_role,
|
|
430
|
-
classification: classification,
|
|
431
|
-
conflict: selected_role.nil?
|
|
432
|
-
)
|
|
433
|
-
end.freeze
|
|
434
|
-
end
|
|
435
|
-
|
|
436
|
-
def side_change(base, side)
|
|
437
|
-
entry_state(base) == entry_state(side) ? :unchanged : change_kind(base, side)
|
|
438
|
-
end
|
|
439
|
-
|
|
440
|
-
def unmanaged_unchanged?(documents)
|
|
441
|
-
anchor_keys = documents.values.map { |document| document.by_key.keys }.reduce(&:intersection)
|
|
442
|
-
signatures = documents.transform_values { |document| unmanaged_signature(document, anchor_keys) }
|
|
443
|
-
signatures[:base] == signatures[:ours] && signatures[:base] == signatures[:theirs]
|
|
444
|
-
end
|
|
445
|
-
|
|
446
|
-
def unmanaged_signature(document, anchor_keys)
|
|
447
|
-
entries = document.entries.sort_by(&:start_line)
|
|
448
|
-
signature = []
|
|
449
|
-
cursor = 1
|
|
450
|
-
entries.each do |entry|
|
|
451
|
-
if cursor < entry.start_line
|
|
452
|
-
signature << [:source, source_lines(document.source, cursor, entry.start_line - 1)]
|
|
453
|
-
end
|
|
454
|
-
signature << [:entry, entry.key] if anchor_keys.include?(entry.key)
|
|
455
|
-
cursor = entry.end_line + 1
|
|
456
|
-
end
|
|
457
|
-
signature << [:source, source_lines(document.source, cursor, document.source.lines.length)]
|
|
458
|
-
signature
|
|
459
|
-
end
|
|
460
|
-
|
|
461
|
-
def render_composite(request, documents, decisions)
|
|
462
|
-
by_key = decisions.to_h { |decision| [decision.key, decision] }
|
|
463
|
-
fragments = []
|
|
464
|
-
selected = []
|
|
465
|
-
cursor = 1
|
|
466
|
-
documents[:ours].entries.sort_by(&:start_line).each do |ours_entry|
|
|
467
|
-
fragments << range_fragment(:ours, cursor, ours_entry.start_line - 1) if cursor < ours_entry.start_line
|
|
468
|
-
decision = by_key.fetch(ours_entry.key)
|
|
469
|
-
selected_entry_value = decision.selected_role && decision.public_send(decision.selected_role)
|
|
470
|
-
if selected_entry_value
|
|
471
|
-
fragments << entry_fragment(decision.selected_role, selected_entry_value)
|
|
472
|
-
selected << selected_entry(selected_entry_value, decision.selected_role)
|
|
473
|
-
end
|
|
474
|
-
cursor = ours_entry.end_line + 1
|
|
475
|
-
end
|
|
476
|
-
ours_line_count = documents[:ours].source.lines.length
|
|
477
|
-
fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
|
|
478
|
-
decisions.each do |decision|
|
|
479
|
-
next if decision.ours || decision.selected_role != :theirs || !decision.theirs
|
|
480
|
-
|
|
481
|
-
fragments << entry_fragment(:theirs, decision.theirs)
|
|
482
|
-
selected << selected_entry(decision.theirs, :theirs)
|
|
483
|
-
end
|
|
484
|
-
|
|
485
|
-
rendered = render_plan(request, separated_fragments(fragments.compact, documents))
|
|
486
|
-
verification = verify_composite(rendered.content, selected)
|
|
487
|
-
unless verification[:semantic_match]
|
|
488
|
-
return verification_failure(:merge3, request, rendered, decisions, verification)
|
|
489
|
-
end
|
|
490
|
-
|
|
491
|
-
result(
|
|
492
|
-
:merge3,
|
|
493
|
-
request,
|
|
494
|
-
output: rendered.content,
|
|
495
|
-
changes: decisions.map(&:classification),
|
|
496
|
-
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
497
|
-
verification: verification.merge(base_participated: true)
|
|
498
|
-
)
|
|
499
|
-
end
|
|
500
|
-
|
|
501
|
-
def selected_entry(entry, role)
|
|
502
|
-
{
|
|
503
|
-
key: entry.key,
|
|
504
|
-
semantic: entry.semantic,
|
|
505
|
-
attributes: entry.attributes,
|
|
506
|
-
source_role: role,
|
|
507
|
-
source_lines: [entry.start_line, entry.end_line],
|
|
508
|
-
fragment: entry.fragment
|
|
509
|
-
}.freeze
|
|
510
|
-
end
|
|
511
|
-
|
|
512
|
-
def verify_composite(output, selected)
|
|
513
|
-
parsed = analyze_role(:output, output)
|
|
514
|
-
expected_semantics = selected.map { |entry| [entry[:key], entry[:semantic]] }
|
|
515
|
-
expected_attributes = selected.map { |entry| [entry[:key], entry[:attributes]] }
|
|
516
|
-
actual_semantics = parsed.entries.map { |entry| [entry.key, entry.semantic] }
|
|
517
|
-
actual_attributes = parsed.entries.map { |entry| [entry.key, entry.attributes] }
|
|
518
|
-
source_match = parsed.entries.zip(selected).all? do |actual, expected|
|
|
519
|
-
actual.fragment == expected[:fragment] ||
|
|
520
|
-
(!expected[:fragment].end_with?("\n") && actual.fragment == "#{expected[:fragment]}\n")
|
|
521
|
-
end
|
|
522
|
-
{
|
|
523
|
-
output_reparsed: true,
|
|
524
|
-
semantic_match: parsed.issues.empty? && actual_semantics == expected_semantics &&
|
|
525
|
-
actual_attributes == expected_attributes && source_match,
|
|
526
|
-
ast_attributes_match: actual_attributes == expected_attributes,
|
|
527
|
-
source_match: source_match,
|
|
528
|
-
ordered_entries: actual_semantics,
|
|
529
|
-
entry_sources: selected.map { |entry| entry.slice(:key, :source_role, :source_lines) }
|
|
530
|
-
}
|
|
531
|
-
end
|
|
532
|
-
|
|
533
|
-
def ordered_semantics(document)
|
|
534
|
-
document.entries.map { |entry| [entry.key, entry.semantic] }
|
|
535
|
-
end
|
|
536
|
-
|
|
537
|
-
def document_signature(document)
|
|
538
|
-
[ordered_semantics(document), document.entries.map(&:attributes)]
|
|
539
|
-
end
|
|
540
|
-
|
|
541
|
-
def fatal_issues(document)
|
|
542
|
-
document.issues.select { |item| %i[parse_error structural_error].include?(item[:category]) }
|
|
543
|
-
end
|
|
544
|
-
|
|
545
|
-
def render_conflicts(request, documents, decisions, conflicts)
|
|
546
|
-
unless conflicts.all? { |conflict| conflict.ours&.ownership_provable }
|
|
547
|
-
return full_file_conflict(request, documents, decisions, conflicts, :ownership_unproven)
|
|
548
|
-
end
|
|
549
|
-
unless unmanaged_unchanged?(documents)
|
|
550
|
-
return full_file_conflict(request, documents, decisions, conflicts, :unmanaged_source_change)
|
|
551
|
-
end
|
|
552
|
-
|
|
553
|
-
by_key = conflicts.to_h { |conflict| [conflict.key, conflict] }
|
|
554
|
-
fragments = []
|
|
555
|
-
cursor = 1
|
|
556
|
-
documents[:ours].entries.sort_by(&:start_line).each do |entry|
|
|
557
|
-
fragments << range_fragment(:ours, cursor, entry.start_line - 1) if cursor < entry.start_line
|
|
558
|
-
conflict = by_key[entry.key]
|
|
559
|
-
fragments << (conflict ? conflict_fragment(request, conflict) : entry_fragment(:ours, entry))
|
|
560
|
-
cursor = entry.end_line + 1
|
|
561
|
-
end
|
|
562
|
-
ours_line_count = documents[:ours].source.lines.length
|
|
563
|
-
fragments << range_fragment(:ours, cursor, ours_line_count) if cursor <= ours_line_count
|
|
564
|
-
rendered = render_plan(request, fragments.compact)
|
|
565
|
-
conflict_failure(request, decisions, conflicts, rendered, :mapping_entry_localized_conflict)
|
|
566
|
-
end
|
|
567
|
-
|
|
568
|
-
def conflict_fragment(request, decision)
|
|
569
|
-
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
570
|
-
conflict_id: conflict_id(decision.key),
|
|
571
|
-
base: conflict_side(:base, decision.base),
|
|
572
|
-
ours: conflict_side(:ours, decision.ours),
|
|
573
|
-
theirs: conflict_side(:theirs, decision.theirs),
|
|
574
|
-
labels: request.fetch(:labels, {}),
|
|
575
|
-
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
576
|
-
metadata: { path: decision.classification[:path], category: conflict_category(decision) }
|
|
577
|
-
)
|
|
578
|
-
end
|
|
579
|
-
|
|
580
|
-
def conflict_side(role, entry)
|
|
581
|
-
return [] unless entry
|
|
582
|
-
|
|
583
|
-
[entry_fragment(role, entry, line_bounded: true)]
|
|
584
|
-
end
|
|
585
|
-
|
|
586
|
-
def conflict_category(decision)
|
|
587
|
-
decision.ours.nil? || decision.theirs.nil? ? :delete_edit : :edit_edit
|
|
588
|
-
end
|
|
589
|
-
|
|
590
|
-
def conflict_id(key)
|
|
591
|
-
"yaml-mapping-#{Digest::SHA256.hexdigest(key)[0, 16]}"
|
|
592
|
-
end
|
|
593
|
-
|
|
594
|
-
def conflict_failure(request, decisions, conflicts, rendered, strategy)
|
|
595
|
-
failure(
|
|
596
|
-
:merge3,
|
|
597
|
-
request,
|
|
598
|
-
category: :merge_conflict,
|
|
599
|
-
message: 'YAML mapping entries changed incompatibly.',
|
|
600
|
-
changes: decisions.map(&:classification),
|
|
601
|
-
conflicts: conflicts.map { |decision| conflict_payload(decision) },
|
|
602
|
-
conflicted_output: rendered.content,
|
|
603
|
-
conflicted_source: rendered.content,
|
|
604
|
-
render_report: render_report(rendered, strategy),
|
|
605
|
-
verification: { base_participated: true }
|
|
606
|
-
)
|
|
607
|
-
end
|
|
608
|
-
|
|
609
|
-
def structural_conflict(request, documents, unsafe)
|
|
610
|
-
source_role = unsafe.first.role
|
|
611
|
-
full_file_conflict(
|
|
612
|
-
request,
|
|
613
|
-
documents,
|
|
614
|
-
safe_decisions(documents),
|
|
615
|
-
[],
|
|
616
|
-
:structural_ambiguity,
|
|
617
|
-
diagnostics: [
|
|
618
|
-
{
|
|
619
|
-
category: unsafe.first.issues.first[:category],
|
|
620
|
-
severity: :error,
|
|
621
|
-
message: "#{source_role} YAML input is unsafe for structural merge.",
|
|
622
|
-
blocking: true,
|
|
623
|
-
source_role: source_role
|
|
624
|
-
},
|
|
625
|
-
*diagnostics_for(*unsafe)
|
|
626
|
-
]
|
|
627
|
-
)
|
|
628
|
-
end
|
|
629
|
-
|
|
630
|
-
def safe_decisions(documents)
|
|
631
|
-
return [] unless documents.values.all? { |document| document.issues.empty? }
|
|
632
|
-
|
|
633
|
-
three_way_decisions(documents)
|
|
634
|
-
end
|
|
635
|
-
|
|
636
|
-
def unmanaged_conflict(request, documents)
|
|
637
|
-
full_file_conflict(
|
|
638
|
-
request,
|
|
639
|
-
documents,
|
|
640
|
-
three_way_decisions(documents),
|
|
641
|
-
[],
|
|
642
|
-
:unmanaged_source_change,
|
|
643
|
-
diagnostics: [{
|
|
644
|
-
category: :unmanaged_source_change,
|
|
645
|
-
severity: :error,
|
|
646
|
-
message: 'Unmanaged YAML layout or comments changed independently.',
|
|
647
|
-
blocking: true
|
|
648
|
-
}]
|
|
649
|
-
)
|
|
650
|
-
end
|
|
651
|
-
|
|
652
|
-
def full_file_conflict(request, documents, decisions, conflicts, reason, diagnostics: nil)
|
|
653
|
-
conflict = { conflict_id: 'yaml-conflict-root', category: reason, path: '<document>' }.freeze
|
|
654
|
-
rendered = render_plan(
|
|
655
|
-
request,
|
|
656
|
-
[
|
|
657
|
-
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
658
|
-
conflict_id: conflict[:conflict_id],
|
|
659
|
-
base: conflict_source(:base, documents[:base].source),
|
|
660
|
-
ours: conflict_source(:ours, documents[:ours].source),
|
|
661
|
-
theirs: conflict_source(:theirs, documents[:theirs].source),
|
|
662
|
-
labels: request.fetch(:labels, {}),
|
|
663
|
-
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
664
|
-
metadata: { category: reason }
|
|
665
|
-
)
|
|
666
|
-
]
|
|
667
|
-
)
|
|
668
|
-
failure(
|
|
669
|
-
:merge3,
|
|
670
|
-
request,
|
|
671
|
-
category: reason,
|
|
672
|
-
message: 'YAML source cannot be safely combined structurally.',
|
|
673
|
-
diagnostics: diagnostics,
|
|
674
|
-
changes: decisions.map(&:classification),
|
|
675
|
-
conflicts: conflicts.empty? ? [conflict] : conflicts.map { |item| conflict_payload(item) },
|
|
676
|
-
fallbacks: [{ from: :mapping_entry_composite, to: :full_file_conflict, reason: reason }],
|
|
677
|
-
conflicted_output: rendered.content,
|
|
678
|
-
conflicted_source: rendered.content,
|
|
679
|
-
render_report: render_report(rendered, :full_file_conflict),
|
|
680
|
-
verification: { base_participated: true },
|
|
681
|
-
source_role: diagnostics&.first&.fetch(:source_role, nil)
|
|
682
|
-
)
|
|
683
|
-
end
|
|
684
|
-
|
|
685
|
-
def conflict_payload(decision)
|
|
686
|
-
{
|
|
687
|
-
conflict_id: conflict_id(decision.key),
|
|
688
|
-
category: conflict_category(decision),
|
|
689
|
-
path: decision.classification[:path],
|
|
690
|
-
change_classification: decision.classification
|
|
691
|
-
}.freeze
|
|
692
|
-
end
|
|
693
|
-
|
|
694
|
-
def conflict_source(role, source)
|
|
695
|
-
return [] if source.empty?
|
|
696
|
-
|
|
697
|
-
[whole_source_fragment(role, source, line_bounded: true)]
|
|
698
|
-
end
|
|
699
|
-
|
|
700
|
-
def separated_fragments(fragments, documents)
|
|
701
|
-
fragments.each_with_index.flat_map do |fragment, index|
|
|
702
|
-
next [fragment] if index == fragments.length - 1
|
|
703
|
-
next [fragment] if fragment.is_a?(Ast::Merge::SourceRender::ConflictFragment)
|
|
704
|
-
|
|
705
|
-
content = fragment_content(fragment, documents)
|
|
706
|
-
next [fragment] if content.empty? || content.end_with?("\n")
|
|
707
|
-
|
|
708
|
-
[
|
|
709
|
-
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
710
|
-
content: "#{content}\n",
|
|
711
|
-
reason: :mapping_entry_separator,
|
|
712
|
-
producer: provider_id,
|
|
713
|
-
metadata: fragment.metadata.merge(copied_source: true)
|
|
714
|
-
)
|
|
715
|
-
]
|
|
716
|
-
end
|
|
717
|
-
end
|
|
718
|
-
|
|
719
|
-
def fragment_content(fragment, documents)
|
|
720
|
-
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
721
|
-
|
|
722
|
-
document = documents.fetch(fragment.revision)
|
|
723
|
-
source_lines(document.source, fragment.start_line, fragment.end_line)
|
|
724
|
-
end
|
|
725
|
-
|
|
726
|
-
def whole_source_fragment(role, source, line_bounded: false)
|
|
727
|
-
if source.empty?
|
|
728
|
-
return Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
729
|
-
content: '',
|
|
730
|
-
reason: :exact_empty_source,
|
|
731
|
-
producer: provider_id,
|
|
732
|
-
metadata: { source_role: role }
|
|
733
|
-
)
|
|
734
|
-
end
|
|
735
|
-
fragment = range_fragment(role, 1, source.lines.length)
|
|
736
|
-
return fragment unless line_bounded && !source.end_with?("\n")
|
|
737
|
-
|
|
738
|
-
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
739
|
-
content: "#{source}\n",
|
|
740
|
-
reason: :conflict_line_boundary,
|
|
741
|
-
producer: provider_id,
|
|
742
|
-
metadata: { source_role: role, copied_source: true }
|
|
743
|
-
)
|
|
744
|
-
end
|
|
745
|
-
|
|
746
|
-
def entry_fragment(role, entry, line_bounded: false)
|
|
747
|
-
unless line_bounded && !entry.fragment.end_with?("\n")
|
|
748
|
-
return range_fragment(role, entry.start_line, entry.end_line, yaml_key: entry.key)
|
|
749
|
-
end
|
|
750
|
-
|
|
751
|
-
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
752
|
-
content: "#{entry.fragment}\n",
|
|
753
|
-
reason: :conflict_line_boundary,
|
|
754
|
-
producer: provider_id,
|
|
755
|
-
metadata: { source_role: role, yaml_key: entry.key, copied_source: true }
|
|
756
|
-
)
|
|
757
|
-
end
|
|
758
|
-
|
|
759
|
-
def range_fragment(role, start_line, end_line, **metadata)
|
|
760
|
-
return if end_line < start_line
|
|
761
|
-
|
|
762
|
-
Ast::Merge::SourceRender::SourceFragment.new(
|
|
763
|
-
revision: role,
|
|
764
|
-
start_line: start_line,
|
|
765
|
-
end_line: end_line,
|
|
766
|
-
metadata: { source_role: role }.merge(metadata)
|
|
767
|
-
)
|
|
768
|
-
end
|
|
769
|
-
|
|
770
|
-
def source_lines(source, start_line, end_line)
|
|
771
|
-
return '' if end_line < start_line
|
|
772
|
-
|
|
773
|
-
source.lines.slice(start_line - 1, end_line - start_line + 1).to_a.join
|
|
774
|
-
end
|
|
775
|
-
|
|
776
|
-
def render_plan(request, fragments)
|
|
777
|
-
Ast::Merge::SourceRender::Renderer.new.render(
|
|
778
|
-
Ast::Merge::SourceRender::Plan.new(
|
|
779
|
-
sources: {
|
|
780
|
-
base: request[:base_source].to_s,
|
|
781
|
-
ours: request[:ours_source] || request[:current_source].to_s,
|
|
782
|
-
theirs: request[:theirs_source] || request[:incoming_source].to_s
|
|
783
|
-
},
|
|
784
|
-
fragments: fragments,
|
|
785
|
-
metadata: { provider_id: provider_id }
|
|
786
|
-
)
|
|
787
|
-
)
|
|
788
|
-
end
|
|
789
|
-
|
|
790
|
-
def render_report(rendered, strategy)
|
|
791
|
-
{
|
|
792
|
-
strategy: strategy,
|
|
793
|
-
line_records: rendered.line_records,
|
|
794
|
-
synthesized_fragments: rendered.synthesized_fragments,
|
|
795
|
-
conflicts: rendered.conflicts,
|
|
796
|
-
verification_input: rendered.verification_input
|
|
797
|
-
}
|
|
798
|
-
end
|
|
799
|
-
|
|
800
|
-
def issue(category, message, **metadata)
|
|
801
|
-
{ category: category, message: message, **metadata }.freeze
|
|
802
|
-
end
|
|
803
|
-
|
|
804
|
-
def diagnostics_for(*documents)
|
|
805
|
-
documents.flat_map do |document|
|
|
806
|
-
document.issues.map do |item|
|
|
807
|
-
item.merge(severity: :warning, blocking: false, source_role: document.role).freeze
|
|
808
|
-
end
|
|
809
|
-
end.freeze
|
|
810
|
-
end
|
|
811
|
-
|
|
812
|
-
def invalid_document_failure(operation, request, documents)
|
|
813
|
-
first = documents.first
|
|
814
|
-
failure(
|
|
815
|
-
operation,
|
|
816
|
-
request,
|
|
817
|
-
category: first.issues.first[:category],
|
|
818
|
-
message: "#{first.role} YAML input is unsafe",
|
|
819
|
-
diagnostics: diagnostics_for(*documents).map { |diagnostic| diagnostic.merge(blocking: true) },
|
|
820
|
-
source_role: first.role
|
|
821
|
-
)
|
|
822
|
-
end
|
|
823
|
-
|
|
824
|
-
def verification_failure(operation, request, rendered, decisions, verification)
|
|
825
|
-
failure(
|
|
826
|
-
operation,
|
|
827
|
-
request,
|
|
828
|
-
category: :verification_failure,
|
|
829
|
-
message: 'Rendered YAML did not preserve planned ordered semantics, AST attributes, and source fragments.',
|
|
830
|
-
changes: decisions.map(&:classification),
|
|
831
|
-
render_report: render_report(rendered, :exact_mapping_entry_composite),
|
|
832
|
-
verification: operation == :merge3 ? verification.merge(base_participated: true) : verification
|
|
833
|
-
)
|
|
834
|
-
end
|
|
835
|
-
|
|
836
|
-
def result(operation, request, diagnostics: [], changes: [], conflicts: [], fallbacks: [],
|
|
837
|
-
render_report: {}, verification: {}, **payload)
|
|
838
|
-
Ast::Merge::ProviderResult.build(
|
|
839
|
-
operation: operation,
|
|
840
|
-
success: true,
|
|
841
|
-
envelope: envelope(
|
|
842
|
-
request,
|
|
843
|
-
diagnostics: diagnostics,
|
|
844
|
-
changes: changes,
|
|
845
|
-
conflicts: conflicts,
|
|
846
|
-
fallbacks: fallbacks,
|
|
847
|
-
render_report: render_report,
|
|
848
|
-
verification: verification
|
|
849
|
-
),
|
|
850
|
-
**payload
|
|
851
|
-
)
|
|
852
|
-
end
|
|
853
|
-
|
|
854
|
-
def failure(operation, request, category:, message:, diagnostics: nil, changes: [], conflicts: [],
|
|
855
|
-
fallbacks: [], render_report: {}, verification: {}, **payload)
|
|
856
|
-
diagnostics ||= [{ category: category, severity: :error, message: message, blocking: true }]
|
|
857
|
-
Ast::Merge::ProviderResult.build(
|
|
858
|
-
operation: operation,
|
|
859
|
-
success: false,
|
|
860
|
-
envelope: envelope(
|
|
861
|
-
request,
|
|
862
|
-
diagnostics: diagnostics,
|
|
863
|
-
changes: changes,
|
|
864
|
-
conflicts: conflicts,
|
|
865
|
-
fallbacks: fallbacks,
|
|
866
|
-
render_report: render_report,
|
|
867
|
-
verification: verification
|
|
868
|
-
),
|
|
869
|
-
**payload
|
|
870
|
-
)
|
|
871
|
-
end
|
|
872
|
-
|
|
873
|
-
def envelope(request, **fields)
|
|
874
|
-
{
|
|
875
|
-
provider: {
|
|
876
|
-
provider_id: provider_id,
|
|
877
|
-
family: family,
|
|
878
|
-
dialect: request[:dialect] || :yaml,
|
|
879
|
-
backend: request[:backend] || :psych,
|
|
880
|
-
package: PACKAGE_NAME,
|
|
881
|
-
package_version: Psych::Merge::Version::VERSION
|
|
882
|
-
},
|
|
883
|
-
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
884
|
-
}.merge(fields)
|
|
167
|
+
NativeProjection.attributes(node)
|
|
885
168
|
end
|
|
886
169
|
end
|
|
887
170
|
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Psych
|
|
4
|
+
module Merge
|
|
5
|
+
# Portable native Psych AST facts retained across workflow host boundaries.
|
|
6
|
+
module ProviderExtension
|
|
7
|
+
SCHEMA = 'structuredmerge.extension/ruby-psych/v1'
|
|
8
|
+
CAPABILITIES = %w[aliases anchors documents scalar-styles tags].freeze
|
|
9
|
+
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(analysis: nil, tree: nil, **)
|
|
13
|
+
native_tree = analysis&.ast || tree&.inner_tree
|
|
14
|
+
raise ArgumentError, 'Psych provider extension requires an analysis or TreeHaver tree' unless native_tree
|
|
15
|
+
|
|
16
|
+
{
|
|
17
|
+
schema: SCHEMA,
|
|
18
|
+
namespace: 'ruby-psych',
|
|
19
|
+
capabilities: CAPABILITIES,
|
|
20
|
+
payload: {
|
|
21
|
+
ast: NativeProjection.attribute_tree(native_tree),
|
|
22
|
+
semantic: NativeProjection.semantic(native_tree),
|
|
23
|
+
native_tree_visibility: 'provider_internal'
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'psych/merge'
|
|
4
|
+
|
|
5
|
+
module Psych
|
|
6
|
+
module Merge
|
|
7
|
+
module RSpec
|
|
8
|
+
# Native Psych AST attributes retained by the workflow provider.
|
|
9
|
+
module ProviderSnapshotExtension
|
|
10
|
+
module_function
|
|
11
|
+
|
|
12
|
+
def call(**arguments)
|
|
13
|
+
ProviderExtension.call(**arguments)
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
data/lib/psych/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Psych
|
|
|
5
5
|
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
7
|
# Current gem version.
|
|
8
|
-
VERSION = '7.1.
|
|
8
|
+
VERSION = '7.1.8'
|
|
9
9
|
end
|
|
10
10
|
# Current gem version exposed at the traditional constant location.
|
|
11
11
|
VERSION = Version::VERSION # Traditional Constant Location
|
data/lib/psych/merge.rb
CHANGED
|
@@ -104,12 +104,14 @@ module Psych
|
|
|
104
104
|
autoload :FileAnalysis, 'psych/merge/file_analysis'
|
|
105
105
|
autoload :MappingEntry, 'psych/merge/file_analysis'
|
|
106
106
|
autoload :MergeResult, 'psych/merge/merge_result'
|
|
107
|
+
autoload :NativeProjection, 'psych/merge/native_projection'
|
|
107
108
|
autoload :NodeTypeNormalizer, 'psych/merge/node_type_normalizer'
|
|
108
109
|
autoload :NodeWrapper, 'psych/merge/node_wrapper'
|
|
109
110
|
autoload :ConflictResolver, 'psych/merge/conflict_resolver'
|
|
110
111
|
autoload :PartialTemplateMerger, 'psych/merge/partial_template_merger'
|
|
111
112
|
autoload :SmartMerger, 'psych/merge/smart_merger'
|
|
112
113
|
autoload :MappingMatchRefiner, 'psych/merge/mapping_match_refiner'
|
|
114
|
+
autoload :ProviderExtension, 'psych/merge/provider_extension'
|
|
113
115
|
|
|
114
116
|
class << self
|
|
115
117
|
def yaml_feature_profile
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: psych-merge
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 7.1.
|
|
4
|
+
version: 7.1.8
|
|
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.8
|
|
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.8
|
|
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.8
|
|
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.8
|
|
68
68
|
- !ruby/object:Gem::Dependency
|
|
69
69
|
name: version_gem
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -91,14 +91,14 @@ dependencies:
|
|
|
91
91
|
requirements:
|
|
92
92
|
- - '='
|
|
93
93
|
- !ruby/object:Gem::Version
|
|
94
|
-
version: 7.1.
|
|
94
|
+
version: 7.1.8
|
|
95
95
|
type: :runtime
|
|
96
96
|
prerelease: false
|
|
97
97
|
version_requirements: !ruby/object:Gem::Requirement
|
|
98
98
|
requirements:
|
|
99
99
|
- - '='
|
|
100
100
|
- !ruby/object:Gem::Version
|
|
101
|
-
version: 7.1.
|
|
101
|
+
version: 7.1.8
|
|
102
102
|
- !ruby/object:Gem::Dependency
|
|
103
103
|
name: kettle-dev
|
|
104
104
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -108,7 +108,7 @@ dependencies:
|
|
|
108
108
|
version: '3.0'
|
|
109
109
|
- - ">="
|
|
110
110
|
- !ruby/object:Gem::Version
|
|
111
|
-
version: 3.0.
|
|
111
|
+
version: 3.0.31
|
|
112
112
|
type: :development
|
|
113
113
|
prerelease: false
|
|
114
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -118,7 +118,7 @@ dependencies:
|
|
|
118
118
|
version: '3.0'
|
|
119
119
|
- - ">="
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version: 3.0.
|
|
121
|
+
version: 3.0.31
|
|
122
122
|
- !ruby/object:Gem::Dependency
|
|
123
123
|
name: bundler-audit
|
|
124
124
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -196,7 +196,7 @@ dependencies:
|
|
|
196
196
|
version: '3.2'
|
|
197
197
|
- - ">="
|
|
198
198
|
- !ruby/object:Gem::Version
|
|
199
|
-
version: 3.2.
|
|
199
|
+
version: 3.2.3
|
|
200
200
|
type: :development
|
|
201
201
|
prerelease: false
|
|
202
202
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -206,7 +206,7 @@ dependencies:
|
|
|
206
206
|
version: '3.2'
|
|
207
207
|
- - ">="
|
|
208
208
|
- !ruby/object:Gem::Version
|
|
209
|
-
version: 3.2.
|
|
209
|
+
version: 3.2.3
|
|
210
210
|
- !ruby/object:Gem::Dependency
|
|
211
211
|
name: kettle-test
|
|
212
212
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -236,7 +236,7 @@ dependencies:
|
|
|
236
236
|
version: '3.2'
|
|
237
237
|
- - ">="
|
|
238
238
|
- !ruby/object:Gem::Version
|
|
239
|
-
version: 3.2.
|
|
239
|
+
version: 3.2.7
|
|
240
240
|
type: :development
|
|
241
241
|
prerelease: false
|
|
242
242
|
version_requirements: !ruby/object:Gem::Requirement
|
|
@@ -246,7 +246,7 @@ dependencies:
|
|
|
246
246
|
version: '3.2'
|
|
247
247
|
- - ">="
|
|
248
248
|
- !ruby/object:Gem::Version
|
|
249
|
-
version: 3.2.
|
|
249
|
+
version: 3.2.7
|
|
250
250
|
- !ruby/object:Gem::Dependency
|
|
251
251
|
name: ruby-progressbar
|
|
252
252
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -321,10 +321,13 @@ files:
|
|
|
321
321
|
- lib/psych/merge/freeze_node.rb
|
|
322
322
|
- lib/psych/merge/mapping_match_refiner.rb
|
|
323
323
|
- lib/psych/merge/merge_result.rb
|
|
324
|
+
- lib/psych/merge/native_projection.rb
|
|
324
325
|
- lib/psych/merge/node_type_normalizer.rb
|
|
325
326
|
- lib/psych/merge/node_wrapper.rb
|
|
326
327
|
- lib/psych/merge/partial_template_merger.rb
|
|
327
328
|
- lib/psych/merge/provider.rb
|
|
329
|
+
- lib/psych/merge/provider_extension.rb
|
|
330
|
+
- lib/psych/merge/rspec/provider_snapshot_extension.rb
|
|
328
331
|
- lib/psych/merge/smart_merger.rb
|
|
329
332
|
- lib/psych/merge/version.rb
|
|
330
333
|
- sig/psych/merge.rbs
|
|
@@ -334,10 +337,10 @@ licenses:
|
|
|
334
337
|
- PolyForm-Small-Business-1.0.0
|
|
335
338
|
metadata:
|
|
336
339
|
homepage_uri: https://structuredmerge.org
|
|
337
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
338
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
340
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.8
|
|
341
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.8/CHANGELOG.md
|
|
339
342
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
340
|
-
documentation_uri: https://www.rubydoc.info/gems/psych-merge/7.1.
|
|
343
|
+
documentation_uri: https://www.rubydoc.info/gems/psych-merge/7.1.8
|
|
341
344
|
funding_uri: https://github.com/sponsors/pboling
|
|
342
345
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
343
346
|
news_uri: https://www.railsbling.com/tags/psych-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|