dotenv-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/dotenv/merge/file_analysis.rb +20 -2
- data/lib/dotenv/merge/provider.rb +690 -0
- data/lib/dotenv/merge/version.rb +1 -1
- data/lib/dotenv/merge.rb +12 -0
- data/sig/dotenv/merge.rbs +19 -0
- data.tar.gz.sig +0 -0
- metadata +19 -18
- 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: 46e142ff15dc1e96e72670426fb814afdef0bcc45ad9debb6053e0bec292470f
|
|
4
|
+
data.tar.gz: 74b5ab3adb662871181f0127296814062dcb8eb5754a26aeca939431439261c4
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 0cb95e7ceb06abc054212ceb2889924fd7d7f922c58e06803dd1d2d2c15c08142bffb1172352397b3b5c0acf2e36dbcd4bf4101fa6212752bff6572a64c4023e
|
|
7
|
+
data.tar.gz: afaef8d76571e4c56f8479bb4d787e35a2fe5a9648bf5f77995ab11ea8823dface498f83431ee5617fc68d3fee19f54b7904da163e7b097dbe9e707bcebacce2
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/README.md
CHANGED
|
@@ -85,7 +85,7 @@ File.write("merged.env", result.to_s)
|
|
|
85
85
|
|
|
86
86
|
### Compatibility
|
|
87
87
|
|
|
88
|
-
Compatible with MRI Ruby 4.0.0+,
|
|
88
|
+
Compatible with MRI Ruby 4.0.0+, JRuby, and TruffleRuby.
|
|
89
89
|
CI workflows and Appraisals are generated for MRI Ruby 4.0.0+.
|
|
90
90
|
This test floor is configured by `ruby.test_minimum` in `.kettle-jem.yml` and
|
|
91
91
|
may be higher than the gem's runtime compatibility floor when legacy Rubies are
|
|
@@ -223,20 +223,12 @@ See [SECURITY.md][🔐security].
|
|
|
223
223
|
## 🤝 Contributing
|
|
224
224
|
|
|
225
225
|
If you need some ideas of where to help, you could work on adding more code coverage,
|
|
226
|
-
|
|
227
|
-
or use the gem and think about how it could be better.
|
|
226
|
+
check [issues][🤝gh-issues] or [PRs][🤝gh-pulls], or use the gem and think about how it could be better.
|
|
228
227
|
|
|
229
228
|
We [![Keep A Changelog][📗keep-changelog-img]][📗keep-changelog] so if you make changes, remember to update it.
|
|
230
229
|
|
|
231
230
|
See [CONTRIBUTING.md][🤝contributing] for more detailed instructions.
|
|
232
231
|
|
|
233
|
-
### Code Coverage
|
|
234
|
-
|
|
235
|
-
<details markdown="1">
|
|
236
|
-
<summary>Coverage service badges</summary>
|
|
237
|
-
|
|
238
|
-
</details>
|
|
239
|
-
|
|
240
232
|
## 📌 Versioning
|
|
241
233
|
|
|
242
234
|
This library follows [![Semantic Versioning 2.0.0][📌semver-img]][📌semver] for its public API where practical.
|
|
@@ -29,7 +29,7 @@ module Dotenv
|
|
|
29
29
|
DEFAULT_FREEZE_TOKEN = 'dotenv-merge'
|
|
30
30
|
|
|
31
31
|
# @return [CommentTracker] Comment tracker for this file
|
|
32
|
-
attr_reader :comment_tracker
|
|
32
|
+
attr_reader :comment_tracker, :structural_diagnostics
|
|
33
33
|
|
|
34
34
|
# Initialize file analysis with dotenv parser
|
|
35
35
|
#
|
|
@@ -41,6 +41,7 @@ module Dotenv
|
|
|
41
41
|
@source = source
|
|
42
42
|
@freeze_token = freeze_token
|
|
43
43
|
@signature_generator = signature_generator
|
|
44
|
+
@structural_diagnostics = []
|
|
44
45
|
# **options captures any additional parameters (e.g., node_typing) for forward compatibility
|
|
45
46
|
|
|
46
47
|
# Parse all lines
|
|
@@ -285,6 +286,11 @@ module Dotenv
|
|
|
285
286
|
case marker[:type]
|
|
286
287
|
when :freeze
|
|
287
288
|
if open_marker
|
|
289
|
+
@structural_diagnostics << {
|
|
290
|
+
category: :freeze_ambiguity,
|
|
291
|
+
line: marker[:line],
|
|
292
|
+
message: 'Nested freeze marker'
|
|
293
|
+
}.freeze
|
|
288
294
|
DebugLogger.warning("Nested freeze block at line #{marker[:line]}, ignoring")
|
|
289
295
|
else
|
|
290
296
|
open_marker = marker
|
|
@@ -299,12 +305,24 @@ module Dotenv
|
|
|
299
305
|
)
|
|
300
306
|
open_marker = nil
|
|
301
307
|
else
|
|
308
|
+
@structural_diagnostics << {
|
|
309
|
+
category: :freeze_ambiguity,
|
|
310
|
+
line: marker[:line],
|
|
311
|
+
message: 'Unfreeze marker without matching freeze marker'
|
|
312
|
+
}.freeze
|
|
302
313
|
DebugLogger.warning("Unfreeze without freeze at line #{marker[:line]}, ignoring")
|
|
303
314
|
end
|
|
304
315
|
end
|
|
305
316
|
end
|
|
306
317
|
|
|
307
|
-
|
|
318
|
+
if open_marker
|
|
319
|
+
@structural_diagnostics << {
|
|
320
|
+
category: :freeze_ambiguity,
|
|
321
|
+
line: open_marker[:line],
|
|
322
|
+
message: 'Unclosed freeze marker'
|
|
323
|
+
}.freeze
|
|
324
|
+
DebugLogger.warning("Unclosed freeze block starting at line #{open_marker[:line]}")
|
|
325
|
+
end
|
|
308
326
|
|
|
309
327
|
blocks
|
|
310
328
|
end
|
|
@@ -0,0 +1,690 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'digest'
|
|
4
|
+
|
|
5
|
+
module Dotenv
|
|
6
|
+
module Merge
|
|
7
|
+
# Source-preserving, base-aware provider for dotenv assignment documents.
|
|
8
|
+
# rubocop:disable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity -- provider planning, rendering, and reporting form one boundary
|
|
9
|
+
class Provider
|
|
10
|
+
DEFAULT_PROFILE = :source_preserving
|
|
11
|
+
DIALECTS = %i[dotenv].freeze
|
|
12
|
+
|
|
13
|
+
Analysis = Data.define(:role, :source, :file, :lines, :assignments, :by_key, :unowned, :issues)
|
|
14
|
+
Decision = Data.define(:key, :base, :ours, :theirs, :selected_role, :classification, :conflict)
|
|
15
|
+
|
|
16
|
+
def provider_id = 'ruby.dotenv'
|
|
17
|
+
def family = 'dotenv'
|
|
18
|
+
|
|
19
|
+
def capabilities
|
|
20
|
+
{
|
|
21
|
+
operations: Ast::Merge::ProviderContract::OPERATIONS,
|
|
22
|
+
dialects: DIALECTS,
|
|
23
|
+
backends: [Dotenv::Merge::BACKEND_REFERENCE.id.to_sym],
|
|
24
|
+
profiles: [DEFAULT_PROFILE],
|
|
25
|
+
role: :workflow,
|
|
26
|
+
source_preservation: %i[exact_source assignment_fragments line_provenance reparse]
|
|
27
|
+
}.freeze
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def analyze(request)
|
|
31
|
+
analysis = analyze_role(:source, request.fetch(:source))
|
|
32
|
+
result(
|
|
33
|
+
:analyze,
|
|
34
|
+
request,
|
|
35
|
+
analysis: analysis_payload(analysis),
|
|
36
|
+
diagnostics: diagnostics_for(analysis),
|
|
37
|
+
verification: { source_parsed: true, structurally_unambiguous: analysis.issues.empty? }
|
|
38
|
+
)
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def diff2(request)
|
|
42
|
+
before = analyze_role(:before, request.fetch(:before_source))
|
|
43
|
+
after = analyze_role(:after, request.fetch(:after_source))
|
|
44
|
+
changes = assignment_changes(before, after)
|
|
45
|
+
result(
|
|
46
|
+
:diff2,
|
|
47
|
+
request,
|
|
48
|
+
diff: { before: analysis_payload(before), after: analysis_payload(after), changes: changes },
|
|
49
|
+
changes: changes,
|
|
50
|
+
diagnostics: diagnostics_for(before, after),
|
|
51
|
+
verification: { before_parsed: true, after_parsed: true }
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def merge2(request)
|
|
56
|
+
incoming = request.fetch(:incoming_source)
|
|
57
|
+
current = request.fetch(:current_source)
|
|
58
|
+
inputs = [
|
|
59
|
+
analyze_role(:incoming, incoming),
|
|
60
|
+
analyze_role(:current, current)
|
|
61
|
+
]
|
|
62
|
+
unsafe = inputs.reject { |analysis| analysis.issues.empty? }
|
|
63
|
+
unless unsafe.empty?
|
|
64
|
+
return unsafe_failure(
|
|
65
|
+
:merge2,
|
|
66
|
+
request,
|
|
67
|
+
unsafe,
|
|
68
|
+
message: 'dotenv merge2 input is structurally ambiguous'
|
|
69
|
+
)
|
|
70
|
+
end
|
|
71
|
+
|
|
72
|
+
merged = SmartMerger.new(incoming, current, add_template_only_nodes: true).merge.to_s
|
|
73
|
+
output = analyze_role(:output, merged)
|
|
74
|
+
unless output.issues.empty?
|
|
75
|
+
return unsafe_failure(
|
|
76
|
+
:merge2,
|
|
77
|
+
request,
|
|
78
|
+
[output],
|
|
79
|
+
message: 'dotenv merge2 output is structurally ambiguous'
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
result(
|
|
84
|
+
:merge2,
|
|
85
|
+
request,
|
|
86
|
+
output: merged,
|
|
87
|
+
render_report: { strategy: :dotenv_smart_merger, synthesis: :family_emission },
|
|
88
|
+
verification: { output_reparsed: true, ordered_assignments: semantic_assignments(output) }
|
|
89
|
+
)
|
|
90
|
+
rescue Dotenv::Merge::ParseError => e
|
|
91
|
+
failure(:merge2, request, category: :parse_error, message: e.message)
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
def merge3(request)
|
|
95
|
+
analyses = %i[base ours theirs].to_h do |role|
|
|
96
|
+
[role, analyze_role(role, request.fetch(:"#{role}_source"))]
|
|
97
|
+
end
|
|
98
|
+
exact_role = exact_revision_role(analyses)
|
|
99
|
+
if exact_role
|
|
100
|
+
winner = analyses.fetch(exact_role)
|
|
101
|
+
return unsafe_conflict(request, analyses, [winner]) unless winner.issues.empty?
|
|
102
|
+
|
|
103
|
+
return render_exact(request, analyses, exact_role)
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
unsafe = analyses.values.reject { |analysis| analysis.issues.empty? }
|
|
107
|
+
return unsafe_conflict(request, analyses, unsafe) unless unsafe.empty?
|
|
108
|
+
return unowned_conflict(request, analyses) unless unowned_unchanged?(analyses)
|
|
109
|
+
|
|
110
|
+
decisions = three_way_decisions(analyses)
|
|
111
|
+
conflicts = decisions.select(&:conflict)
|
|
112
|
+
return render_conflicts(request, analyses, decisions, conflicts) unless conflicts.empty?
|
|
113
|
+
|
|
114
|
+
render_composite(request, analyses, decisions)
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
private
|
|
118
|
+
|
|
119
|
+
def analyze_role(role, source)
|
|
120
|
+
file = FileAnalysis.new(source)
|
|
121
|
+
lines = source.lines.each_index.map { |index| file.line_at(index + 1) }
|
|
122
|
+
assignments = file.all_assignments
|
|
123
|
+
grouped = assignments.group_by(&:key)
|
|
124
|
+
issues = file.structural_diagnostics.dup
|
|
125
|
+
grouped.each do |key, values|
|
|
126
|
+
next if values.one?
|
|
127
|
+
|
|
128
|
+
issues << { category: :duplicate_key, key: key, message: "Duplicate dotenv key #{key}" }.freeze
|
|
129
|
+
end
|
|
130
|
+
lines.select(&:invalid?).each do |line|
|
|
131
|
+
issues << {
|
|
132
|
+
category: :invalid_line,
|
|
133
|
+
line: line.line_number,
|
|
134
|
+
message: "Invalid dotenv line #{line.line_number}"
|
|
135
|
+
}.freeze
|
|
136
|
+
end
|
|
137
|
+
if file.structural_owners.any? { |owner| owner.is_a?(FreezeNode) }
|
|
138
|
+
issues << { category: :freeze_block, message: 'Freeze blocks require an exact one-sided merge' }.freeze
|
|
139
|
+
end
|
|
140
|
+
|
|
141
|
+
Analysis.new(
|
|
142
|
+
role: role,
|
|
143
|
+
source: source,
|
|
144
|
+
file: file,
|
|
145
|
+
lines: lines.freeze,
|
|
146
|
+
assignments: assignments.freeze,
|
|
147
|
+
by_key: grouped.transform_values(&:first).freeze,
|
|
148
|
+
unowned: lines.reject(&:assignment?).map(&:raw).freeze,
|
|
149
|
+
issues: issues.freeze
|
|
150
|
+
)
|
|
151
|
+
end
|
|
152
|
+
|
|
153
|
+
def analysis_payload(analysis)
|
|
154
|
+
{
|
|
155
|
+
dialect: 'dotenv',
|
|
156
|
+
backend: Dotenv::Merge::BACKEND_REFERENCE.id,
|
|
157
|
+
assignments: semantic_assignments(analysis),
|
|
158
|
+
comment_capability: analysis.file.comment_capability.to_h,
|
|
159
|
+
comment_support_style: analysis.file.comment_support_style.to_h,
|
|
160
|
+
structurally_unambiguous: analysis.issues.empty?
|
|
161
|
+
}
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def semantic_assignments(analysis)
|
|
165
|
+
analysis.assignments.map do |line|
|
|
166
|
+
{ key: line.key, value: line.value, export: line.export? }.freeze
|
|
167
|
+
end.freeze
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def line_state(line)
|
|
171
|
+
line && [line.key, line.value, line.export?, line.raw]
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
def exact_revision_role(analyses)
|
|
175
|
+
return :ours if analyses[:ours].source == analyses[:theirs].source
|
|
176
|
+
return :ours if analyses[:base].source == analyses[:theirs].source
|
|
177
|
+
|
|
178
|
+
:theirs if analyses[:base].source == analyses[:ours].source
|
|
179
|
+
end
|
|
180
|
+
|
|
181
|
+
def render_exact(request, analyses, role)
|
|
182
|
+
rendered = render_plan(request, [whole_source_fragment(role, analyses.fetch(role).source)])
|
|
183
|
+
verification = verify_output(rendered.content, analyses.fetch(role).assignments, role)
|
|
184
|
+
result(
|
|
185
|
+
:merge3,
|
|
186
|
+
request,
|
|
187
|
+
output: rendered.content,
|
|
188
|
+
changes: assignment_changes(analyses[:base], analyses.fetch(role)),
|
|
189
|
+
diagnostics: diagnostics_for(*analyses.values),
|
|
190
|
+
render_report: render_report(rendered, :exact_revision),
|
|
191
|
+
verification: verification.merge(base_participated: true)
|
|
192
|
+
)
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
def assignment_changes(before, after)
|
|
196
|
+
ordered_keys(before, after).filter_map do |key|
|
|
197
|
+
left = before.by_key[key]
|
|
198
|
+
right = after.by_key[key]
|
|
199
|
+
next if line_state(left) == line_state(right)
|
|
200
|
+
|
|
201
|
+
{
|
|
202
|
+
path: "/assignments/#{key}",
|
|
203
|
+
ours: :unchanged,
|
|
204
|
+
theirs: change_kind(left, right)
|
|
205
|
+
}.freeze
|
|
206
|
+
end.freeze
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def ordered_keys(*analyses)
|
|
210
|
+
analyses.flat_map { |analysis| analysis.assignments.map(&:key) }.uniq
|
|
211
|
+
end
|
|
212
|
+
|
|
213
|
+
def change_kind(before, after)
|
|
214
|
+
return :added unless before
|
|
215
|
+
return :deleted unless after
|
|
216
|
+
|
|
217
|
+
:edited
|
|
218
|
+
end
|
|
219
|
+
|
|
220
|
+
def three_way_decisions(analyses)
|
|
221
|
+
ordered_keys(analyses[:ours], analyses[:theirs], analyses[:base]).map do |key|
|
|
222
|
+
base = analyses[:base].by_key[key]
|
|
223
|
+
ours = analyses[:ours].by_key[key]
|
|
224
|
+
theirs = analyses[:theirs].by_key[key]
|
|
225
|
+
base_semantic = line_state(base)
|
|
226
|
+
ours_semantic = line_state(ours)
|
|
227
|
+
theirs_semantic = line_state(theirs)
|
|
228
|
+
selected_role =
|
|
229
|
+
if ours_semantic == theirs_semantic
|
|
230
|
+
:ours
|
|
231
|
+
elsif ours_semantic == base_semantic
|
|
232
|
+
:theirs
|
|
233
|
+
elsif theirs_semantic == base_semantic
|
|
234
|
+
:ours
|
|
235
|
+
end
|
|
236
|
+
classification = {
|
|
237
|
+
path: "/assignments/#{key}",
|
|
238
|
+
ours: side_change(base, ours),
|
|
239
|
+
theirs: side_change(base, theirs)
|
|
240
|
+
}.freeze
|
|
241
|
+
Decision.new(
|
|
242
|
+
key: key,
|
|
243
|
+
base: base,
|
|
244
|
+
ours: ours,
|
|
245
|
+
theirs: theirs,
|
|
246
|
+
selected_role: selected_role,
|
|
247
|
+
classification: classification,
|
|
248
|
+
conflict: selected_role.nil?
|
|
249
|
+
)
|
|
250
|
+
end.freeze
|
|
251
|
+
end
|
|
252
|
+
|
|
253
|
+
def side_change(base, side)
|
|
254
|
+
return :unchanged if line_state(base) == line_state(side)
|
|
255
|
+
|
|
256
|
+
change_kind(base, side)
|
|
257
|
+
end
|
|
258
|
+
|
|
259
|
+
def unowned_unchanged?(analyses)
|
|
260
|
+
anchor_keys = analyses.values.map { |analysis| analysis.by_key.keys }.reduce(&:intersection)
|
|
261
|
+
signatures = analyses.transform_values { |analysis| unmanaged_signature(analysis, anchor_keys) }
|
|
262
|
+
signatures[:base] == signatures[:ours] && signatures[:base] == signatures[:theirs]
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
def unmanaged_signature(analysis, anchor_keys)
|
|
266
|
+
analysis.lines.filter_map do |line|
|
|
267
|
+
if line.assignment?
|
|
268
|
+
[:assignment, line.key] if anchor_keys.include?(line.key)
|
|
269
|
+
else
|
|
270
|
+
[:unmanaged, line.raw]
|
|
271
|
+
end
|
|
272
|
+
end
|
|
273
|
+
end
|
|
274
|
+
|
|
275
|
+
def render_composite(request, analyses, decisions)
|
|
276
|
+
by_key = decisions.to_h { |decision| [decision.key, decision] }
|
|
277
|
+
fragments = []
|
|
278
|
+
selected = []
|
|
279
|
+
analyses[:ours].lines.each do |line|
|
|
280
|
+
if line.assignment?
|
|
281
|
+
decision = by_key.fetch(line.key)
|
|
282
|
+
next unless decision.selected_role
|
|
283
|
+
|
|
284
|
+
selected_line = decision.public_send(decision.selected_role)
|
|
285
|
+
next unless selected_line
|
|
286
|
+
|
|
287
|
+
fragments << line_fragment(decision.selected_role, selected_line.line_number, decision.key)
|
|
288
|
+
selected << selected_assignment(decision, selected_line)
|
|
289
|
+
else
|
|
290
|
+
fragments << line_fragment(:ours, line.line_number)
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
decisions.each do |decision|
|
|
294
|
+
next if decision.ours || decision.selected_role != :theirs || !decision.theirs
|
|
295
|
+
|
|
296
|
+
fragments << line_fragment(:theirs, decision.theirs.line_number, decision.key)
|
|
297
|
+
selected << selected_assignment(decision, decision.theirs)
|
|
298
|
+
end
|
|
299
|
+
|
|
300
|
+
rendered = render_plan(request, line_bounded_fragments(fragments, analyses))
|
|
301
|
+
expected = selected.map { |item| item.fetch(:semantic) }
|
|
302
|
+
verification = verify_composite(rendered.content, expected, selected)
|
|
303
|
+
return verification_failure(request, rendered, decisions, verification) unless verification[:semantic_match]
|
|
304
|
+
|
|
305
|
+
result(
|
|
306
|
+
:merge3,
|
|
307
|
+
request,
|
|
308
|
+
output: rendered.content,
|
|
309
|
+
changes: decisions.map(&:classification),
|
|
310
|
+
render_report: render_report(rendered, :exact_assignment_composite),
|
|
311
|
+
verification: verification.merge(base_participated: true)
|
|
312
|
+
)
|
|
313
|
+
end
|
|
314
|
+
|
|
315
|
+
def selected_assignment(decision, line)
|
|
316
|
+
{
|
|
317
|
+
key: decision.key,
|
|
318
|
+
source_role: decision.selected_role,
|
|
319
|
+
source_line: line.line_number,
|
|
320
|
+
source: line.raw,
|
|
321
|
+
semantic: { key: line.key, value: line.value, export: line.export? }.freeze
|
|
322
|
+
}.freeze
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def fragment_content(fragment, analyses)
|
|
326
|
+
return fragment.content if fragment.is_a?(Ast::Merge::SourceRender::SynthesizedFragment)
|
|
327
|
+
|
|
328
|
+
analyses.fetch(fragment.revision).source.lines[fragment.start_line - 1].to_s
|
|
329
|
+
end
|
|
330
|
+
|
|
331
|
+
def line_bounded_fragments(fragments, analyses)
|
|
332
|
+
fragments.each_with_index.flat_map do |fragment, index|
|
|
333
|
+
next [fragment] if index == fragments.length - 1
|
|
334
|
+
next [fragment] if fragment.is_a?(Ast::Merge::SourceRender::ConflictFragment)
|
|
335
|
+
next [fragment] if fragment_content(fragment, analyses).end_with?("\n")
|
|
336
|
+
|
|
337
|
+
content = fragment_content(fragment, analyses)
|
|
338
|
+
[
|
|
339
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
340
|
+
content: "#{content}\n",
|
|
341
|
+
reason: :assignment_separator,
|
|
342
|
+
producer: provider_id,
|
|
343
|
+
metadata: fragment.metadata.merge(copied_source: true)
|
|
344
|
+
)
|
|
345
|
+
]
|
|
346
|
+
end
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
def verify_composite(output, expected, selected)
|
|
350
|
+
parsed = analyze_role(:output, output)
|
|
351
|
+
actual = semantic_assignments(parsed)
|
|
352
|
+
source_match = parsed.assignments.map(&:raw) == selected.map { |item| item.fetch(:source) }
|
|
353
|
+
{
|
|
354
|
+
output_reparsed: true,
|
|
355
|
+
semantic_match: parsed.issues.empty? && actual == expected && source_match,
|
|
356
|
+
source_match: source_match,
|
|
357
|
+
ordered_assignments: actual,
|
|
358
|
+
assignment_sources: selected.map { |item| item.except(:semantic, :source) }
|
|
359
|
+
}
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def verify_output(output, expected_lines, role)
|
|
363
|
+
parsed = analyze_role(:output, output)
|
|
364
|
+
expected = expected_lines.map { |line| { key: line.key, value: line.value, export: line.export? }.freeze }
|
|
365
|
+
{
|
|
366
|
+
output_reparsed: true,
|
|
367
|
+
semantic_match: semantic_assignments(parsed) == expected,
|
|
368
|
+
structurally_unambiguous: parsed.issues.empty?,
|
|
369
|
+
ordered_assignments: semantic_assignments(parsed),
|
|
370
|
+
source_role: role
|
|
371
|
+
}
|
|
372
|
+
end
|
|
373
|
+
|
|
374
|
+
def render_conflicts(request, analyses, decisions, conflicts)
|
|
375
|
+
if conflicts.any? { |item| item.ours.nil? }
|
|
376
|
+
return full_file_conflict(
|
|
377
|
+
request,
|
|
378
|
+
analyses,
|
|
379
|
+
decisions,
|
|
380
|
+
conflicts,
|
|
381
|
+
:conflict_not_line_addressable
|
|
382
|
+
)
|
|
383
|
+
end
|
|
384
|
+
|
|
385
|
+
by_key = conflicts.to_h { |conflict| [conflict.key, conflict] }
|
|
386
|
+
fragments = analyses[:ours].lines.map do |line|
|
|
387
|
+
conflict = line.assignment? && by_key[line.key]
|
|
388
|
+
conflict ? conflict_fragment(request, conflict) : line_fragment(:ours, line.line_number)
|
|
389
|
+
end
|
|
390
|
+
rendered = render_plan(request, fragments)
|
|
391
|
+
conflict_failure(request, decisions, conflicts, rendered, :assignment_localized_conflict)
|
|
392
|
+
end
|
|
393
|
+
|
|
394
|
+
def conflict_fragment(request, decision)
|
|
395
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
396
|
+
conflict_id: conflict_id(decision.key),
|
|
397
|
+
base: conflict_side(request, :base, decision.base),
|
|
398
|
+
ours: conflict_side(request, :ours, decision.ours),
|
|
399
|
+
theirs: conflict_side(request, :theirs, decision.theirs),
|
|
400
|
+
labels: request.fetch(:labels, {}),
|
|
401
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
402
|
+
metadata: { path: decision.classification.fetch(:path), category: conflict_category(decision) }
|
|
403
|
+
)
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
def conflict_side(request, role, line)
|
|
407
|
+
return [] unless line
|
|
408
|
+
|
|
409
|
+
fragment = line_fragment(role, line.line_number, line.key)
|
|
410
|
+
source_line = request.fetch(:"#{role}_source").lines[line.line_number - 1].to_s
|
|
411
|
+
return [fragment] if source_line.end_with?("\n")
|
|
412
|
+
|
|
413
|
+
[
|
|
414
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
415
|
+
content: "#{source_line}\n",
|
|
416
|
+
reason: :conflict_line_boundary,
|
|
417
|
+
producer: provider_id,
|
|
418
|
+
metadata: { source_role: role, assignment_key: line.key, copied_source: true }
|
|
419
|
+
)
|
|
420
|
+
]
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def conflict_category(decision)
|
|
424
|
+
return :delete_edit if decision.ours.nil? || decision.theirs.nil?
|
|
425
|
+
|
|
426
|
+
:edit_edit
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
def conflict_id(key)
|
|
430
|
+
"dotenv-assignment-#{Digest::SHA256.hexdigest(key)[0, 16]}"
|
|
431
|
+
end
|
|
432
|
+
|
|
433
|
+
def conflict_failure(request, decisions, conflicts, rendered, strategy)
|
|
434
|
+
failure(
|
|
435
|
+
:merge3,
|
|
436
|
+
request,
|
|
437
|
+
category: :merge_conflict,
|
|
438
|
+
message: 'Dotenv assignments changed incompatibly.',
|
|
439
|
+
changes: decisions.map(&:classification),
|
|
440
|
+
conflicts: conflicts.map do |decision|
|
|
441
|
+
{
|
|
442
|
+
conflict_id: conflict_id(decision.key),
|
|
443
|
+
category: conflict_category(decision),
|
|
444
|
+
path: decision.classification.fetch(:path),
|
|
445
|
+
change_classification: decision.classification
|
|
446
|
+
}.freeze
|
|
447
|
+
end,
|
|
448
|
+
conflicted_output: rendered.content,
|
|
449
|
+
conflicted_source: rendered.content,
|
|
450
|
+
render_report: render_report(rendered, strategy),
|
|
451
|
+
verification: { base_participated: true }
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def unsafe_conflict(request, analyses, unsafe)
|
|
456
|
+
source_role = unsafe.first.role
|
|
457
|
+
diagnostics = [{
|
|
458
|
+
category: :parse_error,
|
|
459
|
+
severity: :error,
|
|
460
|
+
message: "#{source_role} structural parse is ambiguous.",
|
|
461
|
+
blocking: true,
|
|
462
|
+
source_role: source_role
|
|
463
|
+
}.freeze, *diagnostics_for(*unsafe)]
|
|
464
|
+
full_file_conflict(
|
|
465
|
+
request,
|
|
466
|
+
analyses,
|
|
467
|
+
three_way_decisions(analyses),
|
|
468
|
+
[],
|
|
469
|
+
:structural_ambiguity,
|
|
470
|
+
diagnostics: diagnostics
|
|
471
|
+
)
|
|
472
|
+
end
|
|
473
|
+
|
|
474
|
+
def unowned_conflict(request, analyses)
|
|
475
|
+
full_file_conflict(
|
|
476
|
+
request,
|
|
477
|
+
analyses,
|
|
478
|
+
three_way_decisions(analyses),
|
|
479
|
+
[],
|
|
480
|
+
:unmanaged_source_change,
|
|
481
|
+
diagnostics: [{
|
|
482
|
+
category: :unmanaged_source_change,
|
|
483
|
+
severity: :error,
|
|
484
|
+
message: 'Non-assignment dotenv source changed independently.',
|
|
485
|
+
blocking: true
|
|
486
|
+
}]
|
|
487
|
+
)
|
|
488
|
+
end
|
|
489
|
+
|
|
490
|
+
def full_file_conflict(request, analyses, decisions, conflicts, reason, diagnostics: nil)
|
|
491
|
+
conflict = {
|
|
492
|
+
conflict_id: 'dotenv-conflict-root',
|
|
493
|
+
category: reason,
|
|
494
|
+
path: '<unmanaged-source>'
|
|
495
|
+
}.freeze
|
|
496
|
+
rendered = render_plan(
|
|
497
|
+
request,
|
|
498
|
+
[
|
|
499
|
+
Ast::Merge::SourceRender::ConflictFragment.new(
|
|
500
|
+
conflict_id: conflict.fetch(:conflict_id),
|
|
501
|
+
base: conflict_source(:base, analyses[:base].source),
|
|
502
|
+
ours: conflict_source(:ours, analyses[:ours].source),
|
|
503
|
+
theirs: conflict_source(:theirs, analyses[:theirs].source),
|
|
504
|
+
labels: request.fetch(:labels, {}),
|
|
505
|
+
marker_size: request.fetch(:conflict_marker_size, 7),
|
|
506
|
+
metadata: { category: reason }
|
|
507
|
+
)
|
|
508
|
+
]
|
|
509
|
+
)
|
|
510
|
+
failure(
|
|
511
|
+
:merge3,
|
|
512
|
+
request,
|
|
513
|
+
category: reason,
|
|
514
|
+
message: 'Dotenv source cannot be safely combined structurally.',
|
|
515
|
+
diagnostics: diagnostics,
|
|
516
|
+
changes: decisions.map(&:classification),
|
|
517
|
+
conflicts: conflicts.empty? ? [conflict] : conflicts.map { |item| conflict_payload(item) },
|
|
518
|
+
fallbacks: [{ from: :assignment_composite, to: :full_file_conflict, reason: reason }],
|
|
519
|
+
conflicted_output: rendered.content,
|
|
520
|
+
conflicted_source: rendered.content,
|
|
521
|
+
render_report: render_report(rendered, :full_file_conflict),
|
|
522
|
+
verification: { base_participated: true },
|
|
523
|
+
source_role: diagnostics&.first&.fetch(:source_role, nil)
|
|
524
|
+
)
|
|
525
|
+
end
|
|
526
|
+
|
|
527
|
+
def conflict_source(role, source)
|
|
528
|
+
return [] if source.empty?
|
|
529
|
+
return [whole_source_fragment(role, source)] if source.end_with?("\n")
|
|
530
|
+
|
|
531
|
+
[
|
|
532
|
+
Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
533
|
+
content: "#{source}\n",
|
|
534
|
+
reason: :conflict_line_boundary,
|
|
535
|
+
producer: provider_id,
|
|
536
|
+
metadata: { source_role: role, copied_source: true }
|
|
537
|
+
)
|
|
538
|
+
]
|
|
539
|
+
end
|
|
540
|
+
|
|
541
|
+
def conflict_payload(decision)
|
|
542
|
+
return decision if decision.is_a?(Hash)
|
|
543
|
+
|
|
544
|
+
{
|
|
545
|
+
conflict_id: conflict_id(decision.key),
|
|
546
|
+
category: conflict_category(decision),
|
|
547
|
+
path: decision.classification.fetch(:path),
|
|
548
|
+
change_classification: decision.classification
|
|
549
|
+
}.freeze
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
def whole_source_fragment(role, source)
|
|
553
|
+
if source.empty?
|
|
554
|
+
return Ast::Merge::SourceRender::SynthesizedFragment.new(
|
|
555
|
+
content: '',
|
|
556
|
+
reason: :exact_empty_source,
|
|
557
|
+
producer: provider_id,
|
|
558
|
+
metadata: { source_role: role }
|
|
559
|
+
)
|
|
560
|
+
end
|
|
561
|
+
|
|
562
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
563
|
+
revision: role,
|
|
564
|
+
start_line: 1,
|
|
565
|
+
end_line: source.lines.length,
|
|
566
|
+
metadata: { source_role: role }
|
|
567
|
+
)
|
|
568
|
+
end
|
|
569
|
+
|
|
570
|
+
def line_fragment(role, line_number, key = nil)
|
|
571
|
+
Ast::Merge::SourceRender::SourceFragment.new(
|
|
572
|
+
revision: role,
|
|
573
|
+
start_line: line_number,
|
|
574
|
+
end_line: line_number,
|
|
575
|
+
metadata: { source_role: role, assignment_key: key }.compact
|
|
576
|
+
)
|
|
577
|
+
end
|
|
578
|
+
|
|
579
|
+
def render_plan(request, fragments)
|
|
580
|
+
Ast::Merge::SourceRender::Renderer.new.render(
|
|
581
|
+
Ast::Merge::SourceRender::Plan.new(
|
|
582
|
+
sources: {
|
|
583
|
+
base: request[:base_source].to_s,
|
|
584
|
+
ours: request[:ours_source].to_s,
|
|
585
|
+
theirs: request[:theirs_source].to_s
|
|
586
|
+
},
|
|
587
|
+
fragments: fragments,
|
|
588
|
+
metadata: { provider_id: provider_id }
|
|
589
|
+
)
|
|
590
|
+
)
|
|
591
|
+
end
|
|
592
|
+
|
|
593
|
+
def render_report(rendered, strategy)
|
|
594
|
+
{
|
|
595
|
+
strategy: strategy,
|
|
596
|
+
line_records: rendered.line_records,
|
|
597
|
+
synthesized_fragments: rendered.synthesized_fragments,
|
|
598
|
+
conflicts: rendered.conflicts,
|
|
599
|
+
verification_input: rendered.verification_input
|
|
600
|
+
}
|
|
601
|
+
end
|
|
602
|
+
|
|
603
|
+
def diagnostics_for(*analyses)
|
|
604
|
+
analyses.flat_map do |analysis|
|
|
605
|
+
analysis.issues.map do |issue|
|
|
606
|
+
issue.merge(
|
|
607
|
+
severity: :warning,
|
|
608
|
+
blocking: false,
|
|
609
|
+
source_role: analysis.role
|
|
610
|
+
).freeze
|
|
611
|
+
end
|
|
612
|
+
end.freeze
|
|
613
|
+
end
|
|
614
|
+
|
|
615
|
+
def unsafe_failure(operation, request, analyses, message:)
|
|
616
|
+
failure(
|
|
617
|
+
operation,
|
|
618
|
+
request,
|
|
619
|
+
category: :structural_ambiguity,
|
|
620
|
+
message: message,
|
|
621
|
+
diagnostics: diagnostics_for(*analyses)
|
|
622
|
+
)
|
|
623
|
+
end
|
|
624
|
+
|
|
625
|
+
def verification_failure(request, rendered, decisions, verification)
|
|
626
|
+
failure(
|
|
627
|
+
:merge3,
|
|
628
|
+
request,
|
|
629
|
+
category: :verification_failure,
|
|
630
|
+
message: 'Rendered dotenv assignments did not match the planned ordered semantics.',
|
|
631
|
+
changes: decisions.map(&:classification),
|
|
632
|
+
render_report: render_report(rendered, :exact_assignment_composite),
|
|
633
|
+
verification: verification.merge(base_participated: true)
|
|
634
|
+
)
|
|
635
|
+
end
|
|
636
|
+
|
|
637
|
+
def result(operation, request, diagnostics: [], changes: [], conflicts: [], fallbacks: [],
|
|
638
|
+
render_report: {}, verification: {}, **payload)
|
|
639
|
+
Ast::Merge::ProviderResult.build(
|
|
640
|
+
operation: operation,
|
|
641
|
+
success: true,
|
|
642
|
+
envelope: envelope(
|
|
643
|
+
request,
|
|
644
|
+
diagnostics: diagnostics,
|
|
645
|
+
changes: changes,
|
|
646
|
+
conflicts: conflicts,
|
|
647
|
+
fallbacks: fallbacks,
|
|
648
|
+
render_report: render_report,
|
|
649
|
+
verification: verification
|
|
650
|
+
),
|
|
651
|
+
**payload
|
|
652
|
+
)
|
|
653
|
+
end
|
|
654
|
+
|
|
655
|
+
def failure(operation, request, category:, message:, diagnostics: nil, changes: [], conflicts: [],
|
|
656
|
+
fallbacks: [], render_report: {}, verification: {}, **payload)
|
|
657
|
+
diagnostics ||= [{ category: category, severity: :error, message: message, blocking: true }]
|
|
658
|
+
Ast::Merge::ProviderResult.build(
|
|
659
|
+
operation: operation,
|
|
660
|
+
success: false,
|
|
661
|
+
envelope: envelope(
|
|
662
|
+
request,
|
|
663
|
+
diagnostics: diagnostics,
|
|
664
|
+
changes: changes,
|
|
665
|
+
conflicts: conflicts,
|
|
666
|
+
fallbacks: fallbacks,
|
|
667
|
+
render_report: render_report,
|
|
668
|
+
verification: verification
|
|
669
|
+
),
|
|
670
|
+
**payload
|
|
671
|
+
)
|
|
672
|
+
end
|
|
673
|
+
|
|
674
|
+
def envelope(request, **fields)
|
|
675
|
+
{
|
|
676
|
+
provider: {
|
|
677
|
+
provider_id: provider_id,
|
|
678
|
+
family: family,
|
|
679
|
+
dialect: request[:dialect] || :dotenv,
|
|
680
|
+
backend: request[:backend] || Dotenv::Merge::BACKEND_REFERENCE.id,
|
|
681
|
+
package: 'dotenv-merge',
|
|
682
|
+
package_version: Dotenv::Merge::Version::VERSION
|
|
683
|
+
},
|
|
684
|
+
profile: { profile_id: request[:profile_id] || DEFAULT_PROFILE }
|
|
685
|
+
}.merge(fields)
|
|
686
|
+
end
|
|
687
|
+
end
|
|
688
|
+
# rubocop:enable Metrics/AbcSize, Metrics/BlockLength, Metrics/ClassLength, Metrics/CyclomaticComplexity, Metrics/MethodLength, Metrics/ParameterLists, Metrics/PerceivedComplexity
|
|
689
|
+
end
|
|
690
|
+
end
|
data/lib/dotenv/merge/version.rb
CHANGED
|
@@ -5,7 +5,7 @@ module Dotenv
|
|
|
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/dotenv/merge.rb
CHANGED
|
@@ -69,12 +69,24 @@ module Dotenv
|
|
|
69
69
|
autoload :FreezeNode, 'dotenv/merge/freeze_node'
|
|
70
70
|
autoload :FileAnalysis, 'dotenv/merge/file_analysis'
|
|
71
71
|
autoload :MergeResult, 'dotenv/merge/merge_result'
|
|
72
|
+
autoload :Provider, 'dotenv/merge/provider'
|
|
72
73
|
autoload :SmartMerger, 'dotenv/merge/smart_merger'
|
|
74
|
+
|
|
75
|
+
module_function
|
|
76
|
+
|
|
77
|
+
def merge_provider
|
|
78
|
+
@merge_provider ||= Provider.new
|
|
79
|
+
end
|
|
80
|
+
|
|
81
|
+
def register_provider!(replace: false)
|
|
82
|
+
Ast::Merge.register_provider(merge_provider, replace: replace)
|
|
83
|
+
end
|
|
73
84
|
end
|
|
74
85
|
end
|
|
75
86
|
|
|
76
87
|
require_relative 'merge/backend'
|
|
77
88
|
Dotenv::Merge.register_backend!
|
|
89
|
+
Dotenv::Merge.register_provider!
|
|
78
90
|
|
|
79
91
|
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
80
92
|
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
data/sig/dotenv/merge.rbs
CHANGED
|
@@ -4,5 +4,24 @@ module Dotenv
|
|
|
4
4
|
VERSION: String
|
|
5
5
|
end
|
|
6
6
|
VERSION: String
|
|
7
|
+
|
|
8
|
+
BACKEND_REFERENCE: untyped
|
|
9
|
+
|
|
10
|
+
def self.merge_provider: () -> Provider
|
|
11
|
+
def self.register_provider!: (?replace: bool) -> untyped
|
|
12
|
+
|
|
13
|
+
class Provider
|
|
14
|
+
def provider_id: () -> String
|
|
15
|
+
def family: () -> String
|
|
16
|
+
def capabilities: () -> Hash[Symbol, untyped]
|
|
17
|
+
def analyze: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
18
|
+
def diff2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
19
|
+
def merge2: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
20
|
+
def merge3: (Hash[Symbol, untyped] request) -> Hash[Symbol, untyped]
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
class FileAnalysis
|
|
24
|
+
attr_reader structural_diagnostics: Array[Hash[Symbol, untyped]]
|
|
25
|
+
end
|
|
7
26
|
end
|
|
8
27
|
end
|
data.tar.gz.sig
CHANGED
|
Binary file
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: dotenv-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,42 +43,42 @@ 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: plain-merge
|
|
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: tree_haver
|
|
70
70
|
requirement: !ruby/object:Gem::Requirement
|
|
71
71
|
requirements:
|
|
72
72
|
- - '='
|
|
73
73
|
- !ruby/object:Gem::Version
|
|
74
|
-
version: 7.1.
|
|
74
|
+
version: 7.1.3
|
|
75
75
|
type: :runtime
|
|
76
76
|
prerelease: false
|
|
77
77
|
version_requirements: !ruby/object:Gem::Requirement
|
|
78
78
|
requirements:
|
|
79
79
|
- - '='
|
|
80
80
|
- !ruby/object:Gem::Version
|
|
81
|
-
version: 7.1.
|
|
81
|
+
version: 7.1.3
|
|
82
82
|
- !ruby/object:Gem::Dependency
|
|
83
83
|
name: version_gem
|
|
84
84
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -105,20 +105,20 @@ dependencies:
|
|
|
105
105
|
requirements:
|
|
106
106
|
- - "~>"
|
|
107
107
|
- !ruby/object:Gem::Version
|
|
108
|
-
version: '
|
|
108
|
+
version: '3.0'
|
|
109
109
|
- - ">="
|
|
110
110
|
- !ruby/object:Gem::Version
|
|
111
|
-
version:
|
|
111
|
+
version: 3.0.0
|
|
112
112
|
type: :development
|
|
113
113
|
prerelease: false
|
|
114
114
|
version_requirements: !ruby/object:Gem::Requirement
|
|
115
115
|
requirements:
|
|
116
116
|
- - "~>"
|
|
117
117
|
- !ruby/object:Gem::Version
|
|
118
|
-
version: '
|
|
118
|
+
version: '3.0'
|
|
119
119
|
- - ">="
|
|
120
120
|
- !ruby/object:Gem::Version
|
|
121
|
-
version:
|
|
121
|
+
version: 3.0.0
|
|
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.2
|
|
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.2
|
|
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.5
|
|
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.5
|
|
250
250
|
- !ruby/object:Gem::Dependency
|
|
251
251
|
name: ruby-progressbar
|
|
252
252
|
requirement: !ruby/object:Gem::Requirement
|
|
@@ -320,6 +320,7 @@ files:
|
|
|
320
320
|
- lib/dotenv/merge/file_analysis.rb
|
|
321
321
|
- lib/dotenv/merge/freeze_node.rb
|
|
322
322
|
- lib/dotenv/merge/merge_result.rb
|
|
323
|
+
- lib/dotenv/merge/provider.rb
|
|
323
324
|
- lib/dotenv/merge/smart_merger.rb
|
|
324
325
|
- lib/dotenv/merge/version.rb
|
|
325
326
|
- sig/dotenv/merge.rbs
|
|
@@ -329,10 +330,10 @@ licenses:
|
|
|
329
330
|
- PolyForm-Small-Business-1.0.0
|
|
330
331
|
metadata:
|
|
331
332
|
homepage_uri: https://structuredmerge.org
|
|
332
|
-
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.
|
|
333
|
-
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.
|
|
333
|
+
source_code_uri: https://github.com/structuredmerge/structuredmerge-ruby/tree/v7.1.3
|
|
334
|
+
changelog_uri: https://github.com/structuredmerge/structuredmerge-ruby/blob/v7.1.3/CHANGELOG.md
|
|
334
335
|
bug_tracker_uri: https://github.com/structuredmerge/structuredmerge-ruby/issues
|
|
335
|
-
documentation_uri: https://www.rubydoc.info/gems/dotenv-merge/7.1.
|
|
336
|
+
documentation_uri: https://www.rubydoc.info/gems/dotenv-merge/7.1.3
|
|
336
337
|
funding_uri: https://github.com/sponsors/pboling
|
|
337
338
|
wiki_uri: https://github.com/structuredmerge/structuredmerge-ruby/wiki
|
|
338
339
|
news_uri: https://www.railsbling.com/tags/dotenv-merge
|
metadata.gz.sig
CHANGED
|
Binary file
|