json-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/json/merge/provider.rb +696 -0
- data/lib/json/merge/source_locator.rb +68 -0
- data/lib/json/merge/three_way_decision.rb +168 -0
- data/lib/json/merge/version.rb +1 -1
- data/lib/json/merge.rb +12 -0
- data/sig/json/merge.rbs +46 -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: ff5d2c1fa09edee00c444aedc35377b040f6dc6e07ae160e31243ffa07ebbe70
|
|
4
|
+
data.tar.gz: 8601741a2313c127bb861081460a4ae6e817f247f804e78721a162dfeeb2010b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: ce19e05de329325e569cca1b0fbb80a4933ff2b8a690bcabf8d68111202793f46fc16f6c1434c01497b607d1e5d1440bb13d4e923ab4b1f941a08faf99706037
|
|
7
|
+
data.tar.gz: 0eba7ac69972809823b4478640a66e49b068829a830066fb9c00465298cba8d0b9c23b7cfb4ebd04e582f3aa0f086289fe84efaa4da777d7b4c3026df7d3199d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -107,7 +107,7 @@ File.write("merged.json", result.to_json)
|
|
|
107
107
|
|
|
108
108
|
### Compatibility
|
|
109
109
|
|
|
110
|
-
Compatible with MRI Ruby 4.0.0+,
|
|
110
|
+
Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
|
|
111
111
|
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
|
|
112
112
|
This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
|
|
113
113
|
may be higher than the gem's runtime compatibility floor when legacy Rubies are
|
|
@@ -433,20 +433,12 @@ See [SECURITY.md][🔐security].
|
|
|
433
433
|
## 🤝 Contributing
|
|
434
434
|
|
|
435
435
|
If you need some ideas of where to help, you could work on adding more code coverage,
|
|
436
|
-
|
|
437
|
-
or use the gem and think about how it could be better.
|
|
436
|
+
check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
|
|
438
437
|
|
|
439
438
|
We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
|
|
440
439
|
|
|
441
440
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
442
441
|
|
|
443
|
-
### Code Coverage
|
|
444
|
-
|
|
445
|
-
<details markdown="1">
|
|
446
|
-
<summary>Coverage service badges</summary>
|
|
447
|
-
|
|
448
|
-
</details>
|
|
449
|
-
|
|
450
442
|
## 📌 Versioning
|
|
451
443
|
|
|
452
444
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -0,0 +1,696 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Portable JSON-family provider for Ast::Merge dispatch.
|
|
6
|
+
# rubocop:disable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider operations keep request, rendering, and result-envelope logic together
|
|
7
|
+
class Provider
|
|
8
|
+
DIALECTS = %i[json jsonc json5].freeze
|
|
9
|
+
DEFAULT_PROFILE = :source_preserving
|
|
10
|
+
|
|
11
|
+
def provider_id
|
|
12
|
+
'ruby.json'
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def family
|
|
16
|
+
'json'
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def capabilities
|
|
20
|
+
{
|
|
21
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
22
|
+
dialects: DIALECTS,
|
|
23
|
+
backends: [Json::Merge::TREE_SITTER_BACKEND.id.to_sym],
|
|
24
|
+
profiles: [DEFAULT_PROFILE],
|
|
25
|
+
role: :workflow,
|
|
26
|
+
source_preservation: %i[exact_source family_synthesis line_provenance reparse]
|
|
27
|
+
}.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def analyze(request)
|
|
31
|
+
dialect = dialect_for(request)
|
|
32
|
+
parsed = Json::Merge.parse_json(request.fetch(:source), dialect, backend: request[:backend])
|
|
33
|
+
return parse_failure(:analyze, :source, parsed, request) unless parsed[:ok]
|
|
34
|
+
|
|
35
|
+
result(
|
|
36
|
+
:analyze,
|
|
37
|
+
request,
|
|
38
|
+
analysis: parsed.fetch(:analysis),
|
|
39
|
+
verification: { source_parsed: true }
|
|
40
|
+
)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def diff2(request)
|
|
44
|
+
before = parse_role!(:diff2, request, :before)
|
|
45
|
+
return before if provider_failure?(before)
|
|
46
|
+
|
|
47
|
+
after = parse_role!(:diff2, request, :after)
|
|
48
|
+
return after if provider_failure?(after)
|
|
49
|
+
|
|
50
|
+
changes = diff_values(before, after, path: '')
|
|
51
|
+
result(
|
|
52
|
+
:diff2,
|
|
53
|
+
request,
|
|
54
|
+
diff: { changes: changes, before: before, after: after },
|
|
55
|
+
changes: changes,
|
|
56
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
57
|
+
)
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def merge2(request)
|
|
61
|
+
dialect = dialect_for(request)
|
|
62
|
+
merged = Json::Merge.merge_json(
|
|
63
|
+
request.fetch(:incoming_source),
|
|
64
|
+
request.fetch(:current_source),
|
|
65
|
+
dialect,
|
|
66
|
+
backend: request[:backend]
|
|
67
|
+
)
|
|
68
|
+
return merge2_failure(merged, request) unless merged[:ok]
|
|
69
|
+
|
|
70
|
+
output = merged.fetch(:output)
|
|
71
|
+
verified = parse_output(output, request)
|
|
72
|
+
result(
|
|
73
|
+
:merge2,
|
|
74
|
+
request,
|
|
75
|
+
output: output,
|
|
76
|
+
changes: [],
|
|
77
|
+
render_report: { strategy: :json_smart_merger, synthesis: :family_emission },
|
|
78
|
+
verification: { output_reparsed: true, semantic_value: verified }
|
|
79
|
+
)
|
|
80
|
+
rescue Json::Merge::ParseError => e
|
|
81
|
+
failure(
|
|
82
|
+
:merge2,
|
|
83
|
+
request,
|
|
84
|
+
category: :render_failure,
|
|
85
|
+
message: "JSON family emitter produced invalid output: #{e.message}"
|
|
86
|
+
)
|
|
87
|
+
end
|
|
88
|
+
|
|
89
|
+
def merge3(request)
|
|
90
|
+
values = parse_merge3_roles(request)
|
|
91
|
+
return values if provider_failure?(values)
|
|
92
|
+
|
|
93
|
+
decision = ThreeWayDecision.new.call(**values)
|
|
94
|
+
rendered = render_merge3(decision, request, values)
|
|
95
|
+
return rendered if provider_failure?(rendered)
|
|
96
|
+
|
|
97
|
+
result(
|
|
98
|
+
:merge3,
|
|
99
|
+
request,
|
|
100
|
+
**rendered,
|
|
101
|
+
changes: decision.changes,
|
|
102
|
+
conflicts: decision.conflicts,
|
|
103
|
+
verification: rendered.fetch(:verification).merge(base_participated: true)
|
|
104
|
+
)
|
|
105
|
+
end
|
|
106
|
+
|
|
107
|
+
private
|
|
108
|
+
|
|
109
|
+
def parse_merge3_roles(request)
|
|
110
|
+
%i[base ours theirs].each_with_object({}) do |role, values|
|
|
111
|
+
parsed = parse_role!(:merge3, request, role)
|
|
112
|
+
return parsed if provider_failure?(parsed)
|
|
113
|
+
|
|
114
|
+
values[role] = parsed
|
|
115
|
+
end
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def parse_role!(operation, request, role)
|
|
119
|
+
Json::Merge.json_value_for_source(
|
|
120
|
+
request.fetch(:"#{role}_source"),
|
|
121
|
+
dialect: dialect_for(request),
|
|
122
|
+
backend: request[:backend]
|
|
123
|
+
)
|
|
124
|
+
rescue Json::Merge::ParseError => e
|
|
125
|
+
failure(
|
|
126
|
+
operation,
|
|
127
|
+
request,
|
|
128
|
+
category: :parse_error,
|
|
129
|
+
message: "#{role} parse error: #{e.message}",
|
|
130
|
+
source_role: role
|
|
131
|
+
)
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
def render_merge3(decision, request, values)
|
|
135
|
+
return render_conflict(request, decision) if decision.conflicted?
|
|
136
|
+
|
|
137
|
+
exact_role = %i[ours theirs base].find { |role| values.fetch(role) == decision.value }
|
|
138
|
+
return render_exact_role(exact_role, request, decision) if exact_role
|
|
139
|
+
|
|
140
|
+
render_composite(request, decision)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def diff_values(before, after, path:)
|
|
144
|
+
return [] if before == after
|
|
145
|
+
return diff_objects(before, after, path: path) if before.is_a?(Hash) && after.is_a?(Hash)
|
|
146
|
+
|
|
147
|
+
[{ path: path, ours: :unchanged, theirs: :edited }.freeze]
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def diff_objects(before, after, path:)
|
|
151
|
+
ordered_diff_keys(before, after).flat_map do |key|
|
|
152
|
+
child_path = "#{path}/#{escape_path_segment(key)}"
|
|
153
|
+
if !before.key?(key)
|
|
154
|
+
[{ path: child_path, ours: :unchanged, theirs: :added }.freeze]
|
|
155
|
+
elsif !after.key?(key)
|
|
156
|
+
[{ path: child_path, ours: :unchanged, theirs: :deleted }.freeze]
|
|
157
|
+
else
|
|
158
|
+
diff_values(before.fetch(key), after.fetch(key), path: child_path)
|
|
159
|
+
end
|
|
160
|
+
end.freeze
|
|
161
|
+
end
|
|
162
|
+
|
|
163
|
+
def ordered_diff_keys(before, after)
|
|
164
|
+
[*before.keys, *after.keys].uniq
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def escape_path_segment(key)
|
|
168
|
+
key.to_s.gsub('~', '~0').gsub('/', '~1')
|
|
169
|
+
end
|
|
170
|
+
|
|
171
|
+
def render_exact_role(role, request, decision)
|
|
172
|
+
rendered = render_plan(
|
|
173
|
+
request,
|
|
174
|
+
[
|
|
175
|
+
source_fragment(role, request.fetch(:"#{role}_source"))
|
|
176
|
+
]
|
|
177
|
+
)
|
|
178
|
+
{
|
|
179
|
+
output: rendered.content,
|
|
180
|
+
render_report: render_report(rendered, strategy: :exact_revision),
|
|
181
|
+
verification: verify_rendered(rendered.content, decision.value, request)
|
|
182
|
+
}
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def render_composite(request, decision)
|
|
186
|
+
candidate = Json::Merge.merge_json(
|
|
187
|
+
request.fetch(:theirs_source),
|
|
188
|
+
request.fetch(:ours_source),
|
|
189
|
+
dialect_for(request),
|
|
190
|
+
backend: request[:backend]
|
|
191
|
+
)
|
|
192
|
+
emitted = verified_family_composite(candidate, request, decision)
|
|
193
|
+
return emitted if emitted
|
|
194
|
+
|
|
195
|
+
exact = render_exact_composite(request, decision)
|
|
196
|
+
return exact if exact
|
|
197
|
+
|
|
198
|
+
composite_failure(request, 'JSON family renderers did not reproduce the planned three-way value.')
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
def verified_family_composite(candidate, request, decision)
|
|
202
|
+
return unless candidate[:ok]
|
|
203
|
+
|
|
204
|
+
output = candidate.fetch(:output)
|
|
205
|
+
verification = verify_rendered(output, decision.value, request)
|
|
206
|
+
return unless verification[:semantic_match]
|
|
207
|
+
|
|
208
|
+
rendered = render_plan(
|
|
209
|
+
request,
|
|
210
|
+
[
|
|
211
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
212
|
+
content: output,
|
|
213
|
+
reason: :family_emission,
|
|
214
|
+
producer: provider_id
|
|
215
|
+
)
|
|
216
|
+
]
|
|
217
|
+
)
|
|
218
|
+
{
|
|
219
|
+
output: rendered.content,
|
|
220
|
+
render_report: render_report(rendered, strategy: :family_composite),
|
|
221
|
+
verification: verification
|
|
222
|
+
}
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def render_exact_composite(request, decision)
|
|
226
|
+
replacements = exact_composite_replacements(request, decision)
|
|
227
|
+
return unless replacements
|
|
228
|
+
|
|
229
|
+
rendered = render_plan(request, replacement_fragments(request, replacements))
|
|
230
|
+
verification = verify_rendered(rendered.content, decision.value, request)
|
|
231
|
+
return unless verification[:semantic_match]
|
|
232
|
+
|
|
233
|
+
{
|
|
234
|
+
output: rendered.content,
|
|
235
|
+
render_report: render_report(rendered, strategy: :exact_owner_composite),
|
|
236
|
+
fallbacks: [
|
|
237
|
+
{
|
|
238
|
+
from: :family_composite,
|
|
239
|
+
to: :exact_owner_composite,
|
|
240
|
+
reason: :semantic_verification_failed
|
|
241
|
+
}
|
|
242
|
+
],
|
|
243
|
+
verification: verification
|
|
244
|
+
}
|
|
245
|
+
end
|
|
246
|
+
|
|
247
|
+
def exact_composite_replacements(request, decision)
|
|
248
|
+
locators = conflict_locators(request)
|
|
249
|
+
changes = decision.changes.select do |change|
|
|
250
|
+
change[:ours] == :unchanged && change[:theirs] != :unchanged
|
|
251
|
+
end
|
|
252
|
+
replacements = []
|
|
253
|
+
until changes.empty?
|
|
254
|
+
change = changes.shift
|
|
255
|
+
ours_range = locators.fetch(:ours).pair_range(change.fetch(:path))
|
|
256
|
+
return unless ours_range
|
|
257
|
+
|
|
258
|
+
theirs_range = if change[:theirs] == :deleted
|
|
259
|
+
nil
|
|
260
|
+
else
|
|
261
|
+
locators.fetch(:theirs).pair_range(change.fetch(:path))
|
|
262
|
+
end
|
|
263
|
+
return if change[:theirs] != :deleted && theirs_range.nil?
|
|
264
|
+
|
|
265
|
+
replacements << { ours: ours_range, theirs: theirs_range }
|
|
266
|
+
end
|
|
267
|
+
return unless non_overlapping_ours_ranges?(replacements)
|
|
268
|
+
|
|
269
|
+
replacements
|
|
270
|
+
end
|
|
271
|
+
|
|
272
|
+
def replacement_fragments(request, replacements)
|
|
273
|
+
fragments = []
|
|
274
|
+
cursor = 1
|
|
275
|
+
replacements.sort_by { |item| item.fetch(:ours).start_line }.each do |item|
|
|
276
|
+
ours_range = item.fetch(:ours)
|
|
277
|
+
fragments << source_range_fragment(:ours, cursor, ours_range.start_line - 1) if cursor < ours_range.start_line
|
|
278
|
+
theirs_range = item[:theirs]
|
|
279
|
+
fragments << source_range_fragment(:theirs, theirs_range.start_line, theirs_range.end_line) if theirs_range
|
|
280
|
+
cursor = ours_range.end_line + 1
|
|
281
|
+
end
|
|
282
|
+
final_line = request.fetch(:ours_source).lines.length
|
|
283
|
+
fragments << source_range_fragment(:ours, cursor, final_line) if cursor <= final_line
|
|
284
|
+
fragments
|
|
285
|
+
end
|
|
286
|
+
|
|
287
|
+
def render_conflict(request, decision)
|
|
288
|
+
localized = localized_conflict_plan(request, decision)
|
|
289
|
+
return localized if localized
|
|
290
|
+
|
|
291
|
+
synthesized = synthesized_conflict_plan(request, decision)
|
|
292
|
+
return synthesized if synthesized
|
|
293
|
+
|
|
294
|
+
render_full_file_conflict(request, decision)
|
|
295
|
+
end
|
|
296
|
+
|
|
297
|
+
def localized_conflict_plan(request, decision)
|
|
298
|
+
ranges = localized_conflict_ranges(request, decision)
|
|
299
|
+
return if ranges.nil?
|
|
300
|
+
|
|
301
|
+
rendered = render_plan(request, localized_conflict_fragments(request, decision, ranges))
|
|
302
|
+
conflict_failure(request, decision, rendered, strategy: :owner_localized_conflict)
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def localized_conflict_ranges(request, decision)
|
|
306
|
+
locators = conflict_locators(request)
|
|
307
|
+
ranges = decision.conflicts.map do |conflict|
|
|
308
|
+
conflict_ranges(conflict, locators)
|
|
309
|
+
end
|
|
310
|
+
return if ranges.any?(&:nil?)
|
|
311
|
+
return unless non_overlapping_ours_ranges?(ranges)
|
|
312
|
+
|
|
313
|
+
ranges
|
|
314
|
+
end
|
|
315
|
+
|
|
316
|
+
def synthesized_conflict_plan(request, decision)
|
|
317
|
+
candidate = Json::Merge.merge_json(
|
|
318
|
+
request.fetch(:theirs_source),
|
|
319
|
+
request.fetch(:ours_source),
|
|
320
|
+
dialect_for(request),
|
|
321
|
+
backend: request[:backend]
|
|
322
|
+
)
|
|
323
|
+
return unless candidate[:ok]
|
|
324
|
+
return unless verify_rendered(candidate.fetch(:output), decision.value, request)[:semantic_match]
|
|
325
|
+
|
|
326
|
+
ranges = synthesized_conflict_ranges(request, decision, candidate.fetch(:output))
|
|
327
|
+
return unless ranges
|
|
328
|
+
|
|
329
|
+
rendered = render_plan(
|
|
330
|
+
request,
|
|
331
|
+
synthesized_conflict_fragments(request, decision, candidate.fetch(:output), ranges)
|
|
332
|
+
)
|
|
333
|
+
conflict_failure(
|
|
334
|
+
request,
|
|
335
|
+
decision,
|
|
336
|
+
rendered,
|
|
337
|
+
strategy: :synthesized_owner_localized_conflict,
|
|
338
|
+
fallbacks: [
|
|
339
|
+
{
|
|
340
|
+
from: :exact_owner_localization,
|
|
341
|
+
to: :synthesized_conflict_context,
|
|
342
|
+
reason: :missing_side_owner
|
|
343
|
+
}
|
|
344
|
+
]
|
|
345
|
+
)
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
def synthesized_conflict_ranges(request, decision, candidate)
|
|
349
|
+
locators = conflict_locators(request)
|
|
350
|
+
candidate_locator = SourceLocator.new(
|
|
351
|
+
candidate,
|
|
352
|
+
dialect: dialect_for(request),
|
|
353
|
+
backend: request[:backend]
|
|
354
|
+
)
|
|
355
|
+
conflicts = decision.conflicts.dup
|
|
356
|
+
ranges = []
|
|
357
|
+
until conflicts.empty?
|
|
358
|
+
conflict = conflicts.shift
|
|
359
|
+
sides = conflict_source_ranges(conflict, locators)
|
|
360
|
+
context = candidate_locator.pair_range(conflict.fetch(:path))
|
|
361
|
+
return unless sides && context
|
|
362
|
+
|
|
363
|
+
ranges << sides.merge(context: context)
|
|
364
|
+
end
|
|
365
|
+
return unless non_overlapping_ranges?(ranges.map { |item| item.fetch(:context) })
|
|
366
|
+
|
|
367
|
+
ranges
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def conflict_source_ranges(conflict, locators)
|
|
371
|
+
ranges = {}
|
|
372
|
+
roles = %i[base ours theirs]
|
|
373
|
+
until roles.empty?
|
|
374
|
+
role = roles.shift
|
|
375
|
+
state = conflict.fetch(role)
|
|
376
|
+
range = state.fetch(:present) ? locators.fetch(role).pair_range(conflict.fetch(:path)) : nil
|
|
377
|
+
return if state.fetch(:present) && range.nil?
|
|
378
|
+
|
|
379
|
+
ranges[role] = range
|
|
380
|
+
end
|
|
381
|
+
ranges
|
|
382
|
+
end
|
|
383
|
+
|
|
384
|
+
def synthesized_conflict_fragments(request, decision, candidate, ranges)
|
|
385
|
+
fragments = []
|
|
386
|
+
cursor = 1
|
|
387
|
+
decision.conflicts
|
|
388
|
+
.zip(ranges)
|
|
389
|
+
.sort_by { |_conflict, item| item.fetch(:context).start_line }
|
|
390
|
+
.each do |conflict, item|
|
|
391
|
+
context = item.fetch(:context)
|
|
392
|
+
append_synthesized_context(fragments, candidate, cursor, context.start_line - 1)
|
|
393
|
+
fragments << conflict_fragment(request, conflict, item)
|
|
394
|
+
cursor = context.end_line + 1
|
|
395
|
+
end
|
|
396
|
+
append_synthesized_context(fragments, candidate, cursor, candidate.lines.length)
|
|
397
|
+
fragments
|
|
398
|
+
end
|
|
399
|
+
|
|
400
|
+
def append_synthesized_context(fragments, source, start_line, end_line)
|
|
401
|
+
return if start_line > end_line
|
|
402
|
+
|
|
403
|
+
content = source.lines.slice(start_line - 1, end_line - start_line + 1).join
|
|
404
|
+
fragments << Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
405
|
+
content: content,
|
|
406
|
+
reason: :conflict_context,
|
|
407
|
+
producer: provider_id,
|
|
408
|
+
metadata: { candidate_line_start: start_line }
|
|
409
|
+
)
|
|
410
|
+
end
|
|
411
|
+
|
|
412
|
+
def conflict_locators(request)
|
|
413
|
+
%i[base ours theirs].to_h do |role|
|
|
414
|
+
[
|
|
415
|
+
role,
|
|
416
|
+
SourceLocator.new(
|
|
417
|
+
request.fetch(:"#{role}_source"),
|
|
418
|
+
dialect: dialect_for(request),
|
|
419
|
+
backend: request[:backend]
|
|
420
|
+
)
|
|
421
|
+
]
|
|
422
|
+
end
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
def conflict_ranges(conflict, locators)
|
|
426
|
+
ranges = {}
|
|
427
|
+
roles = %i[base ours theirs]
|
|
428
|
+
until roles.empty?
|
|
429
|
+
role = roles.shift
|
|
430
|
+
state = conflict.fetch(role)
|
|
431
|
+
range = state.fetch(:present) ? locators.fetch(role).pair_range(conflict.fetch(:path)) : nil
|
|
432
|
+
return if state.fetch(:present) && range.nil?
|
|
433
|
+
|
|
434
|
+
ranges[role] = range
|
|
435
|
+
end
|
|
436
|
+
return if ranges[:ours].nil?
|
|
437
|
+
|
|
438
|
+
ranges
|
|
439
|
+
end
|
|
440
|
+
|
|
441
|
+
def non_overlapping_ours_ranges?(ranges)
|
|
442
|
+
non_overlapping_ranges?(ranges.map { |item| item.fetch(:ours) })
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
def non_overlapping_ranges?(ranges)
|
|
446
|
+
ranges
|
|
447
|
+
.sort_by(&:start_line)
|
|
448
|
+
.each_cons(2)
|
|
449
|
+
.none? { |left, right| left.end_line >= right.start_line }
|
|
450
|
+
end
|
|
451
|
+
|
|
452
|
+
def localized_conflict_fragments(request, decision, ranges)
|
|
453
|
+
fragments = []
|
|
454
|
+
cursor = 1
|
|
455
|
+
decision.conflicts
|
|
456
|
+
.zip(ranges)
|
|
457
|
+
.sort_by { |_conflict, item| item.fetch(:ours).start_line }
|
|
458
|
+
.each do |conflict, item|
|
|
459
|
+
ours_range = item.fetch(:ours)
|
|
460
|
+
if cursor < ours_range.start_line
|
|
461
|
+
fragments << source_range_fragment(:ours, cursor, ours_range.start_line - 1)
|
|
462
|
+
end
|
|
463
|
+
fragments << conflict_fragment(request, conflict, item)
|
|
464
|
+
cursor = ours_range.end_line + 1
|
|
465
|
+
end
|
|
466
|
+
final_line = request.fetch(:ours_source).lines.length
|
|
467
|
+
fragments << source_range_fragment(:ours, cursor, final_line) if cursor <= final_line
|
|
468
|
+
fragments
|
|
469
|
+
end
|
|
470
|
+
|
|
471
|
+
def conflict_fragment(request, conflict, ranges)
|
|
472
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
473
|
+
conflict_id: conflict.fetch(:conflict_id),
|
|
474
|
+
base: conflict_side_fragment(:base, ranges[:base]),
|
|
475
|
+
ours: conflict_side_fragment(:ours, ranges[:ours]),
|
|
476
|
+
theirs: conflict_side_fragment(:theirs, ranges[:theirs]),
|
|
477
|
+
labels: request.fetch(:labels, {}),
|
|
478
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
479
|
+
metadata: { path: conflict.fetch(:path), category: conflict.fetch(:category) }
|
|
480
|
+
)
|
|
481
|
+
end
|
|
482
|
+
|
|
483
|
+
def conflict_side_fragment(role, range)
|
|
484
|
+
range ? [source_range_fragment(role, range.start_line, range.end_line)] : []
|
|
485
|
+
end
|
|
486
|
+
|
|
487
|
+
def source_range_fragment(role, start_line, end_line)
|
|
488
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
489
|
+
revision: role,
|
|
490
|
+
start_line: start_line,
|
|
491
|
+
end_line: end_line,
|
|
492
|
+
metadata: { source_role: role }
|
|
493
|
+
)
|
|
494
|
+
end
|
|
495
|
+
|
|
496
|
+
def render_full_file_conflict(request, decision)
|
|
497
|
+
rendered = render_plan(
|
|
498
|
+
request,
|
|
499
|
+
[
|
|
500
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
501
|
+
conflict_id: decision.conflicts.first.fetch(:conflict_id),
|
|
502
|
+
base: [conflict_source_fragment(:base, request.fetch(:base_source))],
|
|
503
|
+
ours: [conflict_source_fragment(:ours, request.fetch(:ours_source))],
|
|
504
|
+
theirs: [conflict_source_fragment(:theirs, request.fetch(:theirs_source))],
|
|
505
|
+
labels: request.fetch(:labels, {}),
|
|
506
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
507
|
+
metadata: { conflicts: decision.conflicts.map { |conflict| conflict[:conflict_id] } }
|
|
508
|
+
)
|
|
509
|
+
]
|
|
510
|
+
)
|
|
511
|
+
conflict_failure(
|
|
512
|
+
request,
|
|
513
|
+
decision,
|
|
514
|
+
rendered,
|
|
515
|
+
strategy: :full_file_conflict,
|
|
516
|
+
fallbacks: [
|
|
517
|
+
{
|
|
518
|
+
from: :owner_localization,
|
|
519
|
+
to: :full_file_conflict,
|
|
520
|
+
reason: :owner_not_whole_line_addressable
|
|
521
|
+
}
|
|
522
|
+
]
|
|
523
|
+
)
|
|
524
|
+
end
|
|
525
|
+
|
|
526
|
+
def conflict_failure(request, decision, rendered, strategy:, fallbacks: [])
|
|
527
|
+
failure(
|
|
528
|
+
:merge3,
|
|
529
|
+
request,
|
|
530
|
+
category: :merge_conflict,
|
|
531
|
+
message: "#{decision.conflicts.length} unresolved JSON merge conflict(s).",
|
|
532
|
+
changes: decision.changes,
|
|
533
|
+
conflicts: decision.conflicts,
|
|
534
|
+
fallbacks: fallbacks,
|
|
535
|
+
conflicted_output: rendered.content,
|
|
536
|
+
render_report: render_report(rendered, strategy: strategy)
|
|
537
|
+
)
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
def render_plan(request, fragments)
|
|
541
|
+
plan = Ast::Merge::SourceRender::Plan.new(
|
|
542
|
+
sources: {
|
|
543
|
+
base: request[:base_source].to_s,
|
|
544
|
+
ours: request[:ours_source].to_s,
|
|
545
|
+
theirs: request[:theirs_source].to_s
|
|
546
|
+
},
|
|
547
|
+
fragments: fragments,
|
|
548
|
+
metadata: { provider_id: provider_id, dialect: dialect_for(request) }
|
|
549
|
+
)
|
|
550
|
+
Ast::Merge::SourceRender::Renderer.new.render(plan)
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
def source_fragment(role, source)
|
|
554
|
+
line_count = source.lines.length
|
|
555
|
+
raise Ast::Merge::SourceRender::InvalidPlanError, "#{role} source has no renderable lines" if line_count.zero?
|
|
556
|
+
|
|
557
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
558
|
+
revision: role,
|
|
559
|
+
start_line: 1,
|
|
560
|
+
end_line: line_count,
|
|
561
|
+
metadata: { source_role: role }
|
|
562
|
+
)
|
|
563
|
+
end
|
|
564
|
+
|
|
565
|
+
def conflict_source_fragment(role, source)
|
|
566
|
+
return source_fragment(role, source) if source.end_with?("\n")
|
|
567
|
+
|
|
568
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
569
|
+
content: "#{source}\n",
|
|
570
|
+
reason: :conflict_line_boundary,
|
|
571
|
+
producer: provider_id,
|
|
572
|
+
metadata: { source_role: role, copied_source: true }
|
|
573
|
+
)
|
|
574
|
+
end
|
|
575
|
+
|
|
576
|
+
def verify_rendered(output, expected_value, request)
|
|
577
|
+
actual = parse_output(output, request)
|
|
578
|
+
{
|
|
579
|
+
output_reparsed: true,
|
|
580
|
+
semantic_match: actual == expected_value
|
|
581
|
+
}
|
|
582
|
+
rescue Json::Merge::ParseError
|
|
583
|
+
{
|
|
584
|
+
output_reparsed: false,
|
|
585
|
+
semantic_match: false
|
|
586
|
+
}
|
|
587
|
+
end
|
|
588
|
+
|
|
589
|
+
def parse_output(output, request)
|
|
590
|
+
Json::Merge.json_value_for_source(
|
|
591
|
+
output,
|
|
592
|
+
dialect: dialect_for(request),
|
|
593
|
+
backend: request[:backend]
|
|
594
|
+
)
|
|
595
|
+
end
|
|
596
|
+
|
|
597
|
+
def result(
|
|
598
|
+
operation, request, verification:, changes: [], conflicts: [], fallbacks: [], render_report: {}, **payload
|
|
599
|
+
)
|
|
600
|
+
Ast::Merge::ProviderResult.build(
|
|
601
|
+
operation: operation,
|
|
602
|
+
success: true,
|
|
603
|
+
envelope: {
|
|
604
|
+
provider: provider_metadata(request),
|
|
605
|
+
profile: profile_metadata(request),
|
|
606
|
+
changes: changes,
|
|
607
|
+
conflicts: conflicts,
|
|
608
|
+
fallbacks: fallbacks,
|
|
609
|
+
render_report: render_report,
|
|
610
|
+
verification: verification
|
|
611
|
+
},
|
|
612
|
+
**payload
|
|
613
|
+
)
|
|
614
|
+
end
|
|
615
|
+
|
|
616
|
+
def failure(
|
|
617
|
+
operation, request, category:, message:, changes: [], conflicts: [], fallbacks: [], render_report: {}, **payload
|
|
618
|
+
)
|
|
619
|
+
Ast::Merge::ProviderResult.build(
|
|
620
|
+
operation: operation,
|
|
621
|
+
success: false,
|
|
622
|
+
envelope: {
|
|
623
|
+
provider: provider_metadata(request),
|
|
624
|
+
profile: profile_metadata(request),
|
|
625
|
+
diagnostics: [{ category: category, severity: :error, message: message, blocking: true }],
|
|
626
|
+
changes: changes,
|
|
627
|
+
conflicts: conflicts,
|
|
628
|
+
fallbacks: fallbacks,
|
|
629
|
+
render_report: render_report,
|
|
630
|
+
verification: { base_participated: operation == :merge3 }
|
|
631
|
+
},
|
|
632
|
+
**payload
|
|
633
|
+
)
|
|
634
|
+
end
|
|
635
|
+
|
|
636
|
+
def parse_failure(operation, role, parsed, request)
|
|
637
|
+
diagnostic = parsed.fetch(:diagnostics).first || {}
|
|
638
|
+
failure(
|
|
639
|
+
operation,
|
|
640
|
+
request,
|
|
641
|
+
category: :parse_error,
|
|
642
|
+
message: "#{role} parse error: #{diagnostic[:message]}",
|
|
643
|
+
source_role: role
|
|
644
|
+
)
|
|
645
|
+
end
|
|
646
|
+
|
|
647
|
+
def merge2_failure(merged, request)
|
|
648
|
+
diagnostic = merged.fetch(:diagnostics).first || {}
|
|
649
|
+
failure(
|
|
650
|
+
:merge2,
|
|
651
|
+
request,
|
|
652
|
+
category: diagnostic[:category] || :merge_failed,
|
|
653
|
+
message: diagnostic[:message] || 'JSON two-way merge failed.'
|
|
654
|
+
)
|
|
655
|
+
end
|
|
656
|
+
|
|
657
|
+
def composite_failure(request, message)
|
|
658
|
+
failure(:merge3, request, category: :render_failure, message: message)
|
|
659
|
+
end
|
|
660
|
+
|
|
661
|
+
def provider_failure?(value)
|
|
662
|
+
value.is_a?(Hash) && value[:ok] == false
|
|
663
|
+
end
|
|
664
|
+
|
|
665
|
+
def render_report(rendered, strategy:)
|
|
666
|
+
{
|
|
667
|
+
strategy: strategy,
|
|
668
|
+
line_records: rendered.line_records,
|
|
669
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
670
|
+
conflicts: rendered.conflicts,
|
|
671
|
+
verification_input: rendered.verification_input
|
|
672
|
+
}
|
|
673
|
+
end
|
|
674
|
+
|
|
675
|
+
def provider_metadata(request)
|
|
676
|
+
{
|
|
677
|
+
provider_id: provider_id,
|
|
678
|
+
family: family,
|
|
679
|
+
dialect: dialect_for(request),
|
|
680
|
+
backend: request[:backend],
|
|
681
|
+
package: Json::Merge::PACKAGE_NAME,
|
|
682
|
+
package_version: Json::Merge::Version::VERSION
|
|
683
|
+
}.compact
|
|
684
|
+
end
|
|
685
|
+
|
|
686
|
+
def profile_metadata(request)
|
|
687
|
+
{ profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
688
|
+
end
|
|
689
|
+
|
|
690
|
+
def dialect_for(request)
|
|
691
|
+
(request[:dialect] || :json).to_sym
|
|
692
|
+
end
|
|
693
|
+
end
|
|
694
|
+
# rubocop:enable Metrics/AbcSize, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
695
|
+
end
|
|
696
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Locates JSON object-pair owners that can be copied as whole source lines.
|
|
6
|
+
class SourceLocator
|
|
7
|
+
Range = Data.define(:start_line, :end_line)
|
|
8
|
+
|
|
9
|
+
def initialize(source, dialect:, backend: nil)
|
|
10
|
+
@source = source
|
|
11
|
+
@lines = source.lines
|
|
12
|
+
Json::Merge.register_backend!
|
|
13
|
+
backend_id = backend.to_s.empty? ? Json::Merge::TREE_SITTER_BACKEND.id : backend.to_s
|
|
14
|
+
@analysis = TreeHaver.with_backend(backend_id) do
|
|
15
|
+
FileAnalysis.new(source, dialect: dialect)
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def pair_range(path)
|
|
20
|
+
pair = pair_for(path)
|
|
21
|
+
return unless pair
|
|
22
|
+
return unless whole_line_owner?(pair)
|
|
23
|
+
|
|
24
|
+
Range.new(start_line: pair.start_line, end_line: pair.end_line)
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def pair_for(path)
|
|
30
|
+
current = @analysis.root_object
|
|
31
|
+
pair = nil
|
|
32
|
+
segments = pointer_segments(path)
|
|
33
|
+
until segments.empty?
|
|
34
|
+
segment = segments.shift
|
|
35
|
+
pair = pair_with_key(current, segment)
|
|
36
|
+
return unless pair
|
|
37
|
+
|
|
38
|
+
current = pair.value_node
|
|
39
|
+
end
|
|
40
|
+
pair
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def pair_with_key(object, key)
|
|
44
|
+
return unless object&.object?
|
|
45
|
+
|
|
46
|
+
object.pairs.find { |candidate| candidate.key_name == key }
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def pointer_segments(path)
|
|
50
|
+
return [] if path.to_s.empty?
|
|
51
|
+
|
|
52
|
+
path.to_s.split('/').drop(1).map do |segment|
|
|
53
|
+
segment.gsub('~1', '/').gsub('~0', '~')
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def whole_line_owner?(pair)
|
|
58
|
+
start_line = pair.start_line
|
|
59
|
+
end_line = pair.end_line
|
|
60
|
+
return false unless start_line && end_line
|
|
61
|
+
return false unless @lines.fetch(end_line - 1, '').end_with?("\n")
|
|
62
|
+
|
|
63
|
+
start_column = pair.node.start_point.fetch(:column)
|
|
64
|
+
@lines.fetch(start_line - 1, '').slice(0, start_column).to_s.strip.empty?
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,168 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Json
|
|
4
|
+
module Merge
|
|
5
|
+
# Computes base-aware JSON-family semantic decisions without rendering.
|
|
6
|
+
# rubocop:disable Metrics/ClassLength -- recursive decisions and classifications form one algorithm
|
|
7
|
+
class ThreeWayDecision
|
|
8
|
+
State = Data.define(:present, :value) do
|
|
9
|
+
def self.present(value)
|
|
10
|
+
new(present: true, value: value)
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def self.missing
|
|
14
|
+
@missing ||= new(present: false, value: nil)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def to_h
|
|
18
|
+
present ? { present: true, value: value } : { present: false }
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
Result = Data.define(:state, :conflicts, :changes) do
|
|
23
|
+
def present?
|
|
24
|
+
state.present
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def value
|
|
28
|
+
state.value
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def conflicted?
|
|
32
|
+
!conflicts.empty?
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def call(base:, ours:, theirs:)
|
|
37
|
+
merge_states(
|
|
38
|
+
State.present(base),
|
|
39
|
+
State.present(ours),
|
|
40
|
+
State.present(theirs),
|
|
41
|
+
path: ''
|
|
42
|
+
)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
private
|
|
46
|
+
|
|
47
|
+
def merge_states(base, ours, theirs, path:)
|
|
48
|
+
return success(ours) if ours == theirs
|
|
49
|
+
return success(theirs, change: classify(path, base, ours, theirs)) if base == ours
|
|
50
|
+
return success(ours, change: classify(path, base, ours, theirs)) if base == theirs
|
|
51
|
+
return merge_added_objects(ours, theirs, path: path) if added_object_states?(base, ours, theirs)
|
|
52
|
+
return merge_objects(base, ours, theirs, path: path) if object_states?(base, ours, theirs)
|
|
53
|
+
|
|
54
|
+
conflict(base, ours, theirs, path: path)
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def merge_objects(base, ours, theirs, path:)
|
|
58
|
+
aggregate = { value: {}, conflicts: [], changes: [] }
|
|
59
|
+
states = [base, ours, theirs]
|
|
60
|
+
ordered_keys(base.value, ours.value, theirs.value).each do |key|
|
|
61
|
+
merge_object_key(aggregate, key, states, path: path)
|
|
62
|
+
end
|
|
63
|
+
|
|
64
|
+
Result.new(
|
|
65
|
+
state: State.present(aggregate[:value]),
|
|
66
|
+
conflicts: aggregate[:conflicts].freeze,
|
|
67
|
+
changes: aggregate[:changes].freeze
|
|
68
|
+
)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def merge_added_objects(ours, theirs, path:)
|
|
72
|
+
merge_objects(State.present({}), ours, theirs, path: path)
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def merge_object_key(aggregate, key, states, path:)
|
|
76
|
+
child_states = states.map { |state| state_for(state.value, key) }
|
|
77
|
+
child = merge_states(*child_states, path: child_path(path, key))
|
|
78
|
+
aggregate[:value][key] = child.value if child.present?
|
|
79
|
+
aggregate[:conflicts].concat(child.conflicts)
|
|
80
|
+
aggregate[:changes].concat(child.changes)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
def success(state, change: nil)
|
|
84
|
+
Result.new(
|
|
85
|
+
state: state,
|
|
86
|
+
conflicts: [].freeze,
|
|
87
|
+
changes: change ? [change].freeze : [].freeze
|
|
88
|
+
)
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
def conflict(base, ours, theirs, path:)
|
|
92
|
+
classification = classify(path, base, ours, theirs)
|
|
93
|
+
selected = ours.present ? ours : theirs
|
|
94
|
+
Result.new(
|
|
95
|
+
state: selected,
|
|
96
|
+
conflicts: [conflict_payload(base, ours, theirs, path, classification)].freeze,
|
|
97
|
+
changes: [classification].freeze
|
|
98
|
+
)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def conflict_payload(base, ours, theirs, path, classification)
|
|
102
|
+
{
|
|
103
|
+
conflict_id: conflict_id(path),
|
|
104
|
+
category: conflict_category(ours, theirs),
|
|
105
|
+
path: path,
|
|
106
|
+
base: base.to_h,
|
|
107
|
+
ours: ours.to_h,
|
|
108
|
+
theirs: theirs.to_h,
|
|
109
|
+
change_classification: classification
|
|
110
|
+
}.freeze
|
|
111
|
+
end
|
|
112
|
+
|
|
113
|
+
def classify(path, base, ours, theirs)
|
|
114
|
+
{
|
|
115
|
+
path: path,
|
|
116
|
+
ours: change_state(base, ours),
|
|
117
|
+
theirs: change_state(base, theirs)
|
|
118
|
+
}.freeze
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
def change_state(base, side)
|
|
122
|
+
return :unchanged if base == side
|
|
123
|
+
return :added unless base.present
|
|
124
|
+
return :deleted unless side.present
|
|
125
|
+
|
|
126
|
+
:edited
|
|
127
|
+
end
|
|
128
|
+
|
|
129
|
+
def conflict_category(ours, theirs)
|
|
130
|
+
ours.present && theirs.present ? :edit_edit : :delete_edit
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
def object_states?(*states)
|
|
134
|
+
states.all? { |state| state.present && state.value.is_a?(Hash) }
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
def added_object_states?(base, ours, theirs)
|
|
138
|
+
!base.present &&
|
|
139
|
+
ours.present && ours.value.is_a?(Hash) &&
|
|
140
|
+
theirs.present && theirs.value.is_a?(Hash)
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def state_for(object, key)
|
|
144
|
+
object.key?(key) ? State.present(object.fetch(key)) : State.missing
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def ordered_keys(*objects)
|
|
148
|
+
objects.each_with_object([]) do |object, keys|
|
|
149
|
+
object.each_key { |key| keys << key unless keys.include?(key) }
|
|
150
|
+
end
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def child_path(path, key)
|
|
154
|
+
"#{path}/#{escape_path_segment(key)}"
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def escape_path_segment(key)
|
|
158
|
+
key.to_s.gsub('~', '~0').gsub('/', '~1')
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
def conflict_id(path)
|
|
162
|
+
suffix = path.delete_prefix('/').gsub(/[^a-zA-Z0-9_-]+/, '-')
|
|
163
|
+
"json-conflict-#{suffix.empty? ? 'root' : suffix}"
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
# rubocop:enable Metrics/ClassLength
|
|
167
|
+
end
|
|
168
|
+
end
|
data/lib/json/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Json
|
|
|
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/json/merge.rb
CHANGED
|
@@ -38,6 +38,9 @@ module Json
|
|
|
38
38
|
autoload :ConflictResolver, 'json/merge/conflict_resolver'
|
|
39
39
|
autoload :SmartMerger, 'json/merge/smart_merger'
|
|
40
40
|
autoload :ObjectMatchRefiner, 'json/merge/object_match_refiner'
|
|
41
|
+
autoload :Provider, 'json/merge/provider'
|
|
42
|
+
autoload :SourceLocator, 'json/merge/source_locator'
|
|
43
|
+
autoload :ThreeWayDecision, 'json/merge/three_way_decision'
|
|
41
44
|
|
|
42
45
|
class << self
|
|
43
46
|
def register_backend!
|
|
@@ -67,6 +70,14 @@ module Json
|
|
|
67
70
|
}
|
|
68
71
|
end
|
|
69
72
|
|
|
73
|
+
def merge_provider
|
|
74
|
+
@merge_provider ||= Provider.new
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def register_provider!(replace: false)
|
|
78
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
79
|
+
end
|
|
80
|
+
|
|
70
81
|
def available_json_backends
|
|
71
82
|
json_backend_available_for_analysis?(TREE_SITTER_BACKEND.id) ? [TREE_SITTER_BACKEND] : []
|
|
72
83
|
end
|
|
@@ -510,6 +521,7 @@ module Json
|
|
|
510
521
|
end
|
|
511
522
|
|
|
512
523
|
Json::Merge.register_backend!
|
|
524
|
+
Json::Merge.register_provider!
|
|
513
525
|
|
|
514
526
|
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
515
527
|
Ast::Merge::RSpec::MergeGemRegistry.register(
|
data/sig/json/merge.rbs
CHANGED
|
@@ -1,5 +1,51 @@
|
|
|
1
1
|
module Json
|
|
2
2
|
module Merge
|
|
3
|
+
class Provider
|
|
4
|
+
def provider_id: () -> String
|
|
5
|
+
def family: () -> String
|
|
6
|
+
def capabilities: () -> Ast::Merge::provider_capabilities
|
|
7
|
+
def analyze: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
8
|
+
def diff2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
9
|
+
def merge2: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
10
|
+
def merge3: (Ast::Merge::provider_request request) -> Ast::Merge::provider_result
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
class SourceLocator
|
|
14
|
+
class Range < Data
|
|
15
|
+
attr_reader start_line: Integer
|
|
16
|
+
attr_reader end_line: Integer
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def initialize: (String source, dialect: Symbol, ?backend: String | Symbol?) -> void
|
|
20
|
+
def pair_range: (String path) -> Range?
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def self.merge_provider: () -> Provider
|
|
24
|
+
def self.register_provider!: (?replace: bool) -> Ast::Merge::_MergeProvider
|
|
25
|
+
|
|
26
|
+
class ThreeWayDecision
|
|
27
|
+
class State < Data
|
|
28
|
+
attr_reader present: bool
|
|
29
|
+
attr_reader value: untyped
|
|
30
|
+
|
|
31
|
+
def self.present: (untyped value) -> State
|
|
32
|
+
def self.missing: () -> State
|
|
33
|
+
def to_h: () -> Hash[Symbol, untyped]
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
class Result < Data
|
|
37
|
+
attr_reader state: State
|
|
38
|
+
attr_reader conflicts: Array[Hash[Symbol, untyped]]
|
|
39
|
+
attr_reader changes: Array[Hash[Symbol, untyped]]
|
|
40
|
+
|
|
41
|
+
def present?: () -> bool
|
|
42
|
+
def value: () -> untyped
|
|
43
|
+
def conflicted?: () -> bool
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def call: (base: untyped, ours: untyped, theirs: untyped) -> Result
|
|
47
|
+
end
|
|
48
|
+
|
|
3
49
|
module Version
|
|
4
50
|
VERSION: String
|
|
5
51
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: json-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,10 @@ files:
|
|
|
308
308
|
- lib/json/merge/merge_result.rb
|
|
309
309
|
- lib/json/merge/node_wrapper.rb
|
|
310
310
|
- lib/json/merge/object_match_refiner.rb
|
|
311
|
+
- lib/json/merge/provider.rb
|
|
311
312
|
- lib/json/merge/smart_merger.rb
|
|
313
|
+
- lib/json/merge/source_locator.rb
|
|
314
|
+
- lib/json/merge/three_way_decision.rb
|
|
312
315
|
- lib/json/merge/version.rb
|
|
313
316
|
- sig/json/merge.rbs
|
|
314
317
|
homepage: https://github.com/structuredmerge/structuredmerge-ruby
|
|
@@ -317,10 +320,10 @@ licenses:
|
|
|
317
320
|
- PolyForm-Small-Business-1.0.0
|
|
318
321
|
metadata:
|
|
319
322
|
homepage_uri: https://structuredmerge.org
|
|
320
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
321
|
-
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
|
|
322
325
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
323
|
-
documentation_uri: https://www.rubydoc.info/gems/json-merge/7.1.
|
|
326
|
+
documentation_uri: https://www.rubydoc.info/gems/json-merge/7.1.3
|
|
324
327
|
funding_uri: https://github.com/sponsors/pboling
|
|
325
328
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
326
329
|
news_uri: https://www.railsbling.com/tags/json-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|