dotenv-merge 1.0.2 → 7.1.1
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/LICENSE.md +13 -0
- data/README.md +101 -515
- data/lib/dotenv/merge/backend.rb +70 -0
- data/lib/dotenv/merge/comment_tracker.rb +112 -0
- data/lib/dotenv/merge/debug_logger.rb +2 -2
- data/lib/dotenv/merge/env_line.rb +10 -14
- data/lib/dotenv/merge/file_analysis.rb +135 -24
- data/lib/dotenv/merge/freeze_node.rb +1 -1
- data/lib/dotenv/merge/merge_result.rb +6 -6
- data/lib/dotenv/merge/smart_merger.rb +360 -137
- data/lib/dotenv/merge/version.rb +5 -4
- data/lib/dotenv/merge.rb +22 -18
- data/lib/dotenv-merge.rb +9 -1
- data/sig/dotenv/merge.rbs +3 -207
- data.tar.gz.sig +0 -0
- metadata +114 -76
- metadata.gz.sig +0 -0
- data/CHANGELOG.md +0 -95
- data/CITATION.cff +0 -20
- data/CODE_OF_CONDUCT.md +0 -134
- data/CONTRIBUTING.md +0 -227
- data/FUNDING.md +0 -74
- data/LICENSE.txt +0 -21
- data/REEK +0 -0
- data/RUBOCOP.md +0 -71
- data/SECURITY.md +0 -21
- data/sig/dotenv/merge/env_line.rbs +0 -90
|
@@ -25,6 +25,11 @@ module Dotenv
|
|
|
25
25
|
# node_typing: { "EnvLine" => ->(n) { NodeTyping.with_merge_type(n, :secret) } },
|
|
26
26
|
# preference: { default: :destination, secret: :template })
|
|
27
27
|
class SmartMerger < ::Ast::Merge::SmartMergerBase
|
|
28
|
+
include Ast::Merge::TrailingGroups::DestIterate
|
|
29
|
+
include Ast::Merge::CommentLayoutEmissionSupport
|
|
30
|
+
|
|
31
|
+
attr_reader :corruption_handling
|
|
32
|
+
|
|
28
33
|
# Initialize a new SmartMerger
|
|
29
34
|
#
|
|
30
35
|
# @param template_content [String] Content of the template dotenv file
|
|
@@ -33,6 +38,10 @@ module Dotenv
|
|
|
33
38
|
# @param preference [Symbol, Hash] :destination, :template, or per-type Hash
|
|
34
39
|
# @param add_template_only_nodes [Boolean] Whether to add template-only env vars
|
|
35
40
|
# (default: false)
|
|
41
|
+
# @param remove_template_missing_nodes [Boolean] Whether to remove destination-only
|
|
42
|
+
# env vars that do not exist in the template (default: false)
|
|
43
|
+
# @param corruption_handling [Symbol] How to handle detected historical
|
|
44
|
+
# duplicate-prefix corruption (:heal, :warn, :error, :skip)
|
|
36
45
|
# @param freeze_token [String] Token for freeze block markers
|
|
37
46
|
# (default: "dotenv-merge")
|
|
38
47
|
# @param match_refiner [#call, nil] Match refiner for fuzzy matching
|
|
@@ -47,6 +56,8 @@ module Dotenv
|
|
|
47
56
|
signature_generator: nil,
|
|
48
57
|
preference: :destination,
|
|
49
58
|
add_template_only_nodes: false,
|
|
59
|
+
remove_template_missing_nodes: false,
|
|
60
|
+
corruption_handling: :heal,
|
|
50
61
|
freeze_token: nil,
|
|
51
62
|
match_refiner: nil,
|
|
52
63
|
regions: nil,
|
|
@@ -54,12 +65,16 @@ module Dotenv
|
|
|
54
65
|
node_typing: nil,
|
|
55
66
|
**options
|
|
56
67
|
)
|
|
68
|
+
@remove_template_missing_nodes = remove_template_missing_nodes
|
|
69
|
+
@corruption_handling = ::Ast::Merge::Healer.normalize_mode(corruption_handling)
|
|
70
|
+
|
|
57
71
|
super(
|
|
58
72
|
template_content,
|
|
59
73
|
dest_content,
|
|
60
74
|
signature_generator: signature_generator,
|
|
61
75
|
preference: preference,
|
|
62
76
|
add_template_only_nodes: add_template_only_nodes,
|
|
77
|
+
remove_template_missing_nodes: remove_template_missing_nodes,
|
|
63
78
|
freeze_token: freeze_token,
|
|
64
79
|
match_refiner: match_refiner,
|
|
65
80
|
regions: regions,
|
|
@@ -78,7 +93,7 @@ module Dotenv
|
|
|
78
93
|
|
|
79
94
|
# @return [String] The default freeze token
|
|
80
95
|
def default_freeze_token
|
|
81
|
-
|
|
96
|
+
'dotenv-merge'
|
|
82
97
|
end
|
|
83
98
|
|
|
84
99
|
# @return [Class, nil] No separate resolver class for dotenv
|
|
@@ -106,151 +121,112 @@ module Dotenv
|
|
|
106
121
|
ParseError
|
|
107
122
|
end
|
|
108
123
|
|
|
109
|
-
# Perform the dotenv-specific merge with
|
|
124
|
+
# Perform the dotenv-specific merge with structural-owner alignment.
|
|
110
125
|
#
|
|
111
126
|
# @return [MergeResult] The merge result
|
|
112
127
|
def perform_merge
|
|
113
|
-
|
|
128
|
+
template_nodes = @template_analysis.structural_owners
|
|
129
|
+
dest_nodes = @dest_analysis.structural_owners
|
|
130
|
+
|
|
131
|
+
emit_root_boundary(:preamble)
|
|
132
|
+
|
|
133
|
+
template_index = build_match_index(template_nodes, @template_analysis)
|
|
134
|
+
dest_sigs = destination_signature_set(dest_nodes, @dest_analysis)
|
|
135
|
+
trailing_groups, matched_indices = build_dest_iterate_trailing_groups(
|
|
136
|
+
template_nodes: template_nodes,
|
|
137
|
+
dest_sigs: dest_sigs,
|
|
138
|
+
signature_for: ->(node) { freeze_node?(node) ? nil : @template_analysis.generate_signature(node) },
|
|
139
|
+
add_template_only_nodes: @add_template_only_nodes
|
|
140
|
+
)
|
|
114
141
|
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
matches: alignment.count { |e| e[:type] == :match },
|
|
118
|
-
template_only: alignment.count { |e| e[:type] == :template_only },
|
|
119
|
-
dest_only: alignment.count { |e| e[:type] == :dest_only },
|
|
120
|
-
})
|
|
142
|
+
matched_template_indices = Set.new
|
|
143
|
+
consumed_indices = Set.new
|
|
121
144
|
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
145
|
+
emit_prefix_trailing_group(trailing_groups, consumed_indices) do |info|
|
|
146
|
+
add_template_only_node(info[:node], template_nodes.index(info[:node]))
|
|
147
|
+
end
|
|
125
148
|
|
|
126
|
-
|
|
149
|
+
dest_nodes.each do |dest_node|
|
|
150
|
+
if freeze_node?(dest_node)
|
|
151
|
+
@result.add_freeze_block(dest_node)
|
|
152
|
+
next
|
|
153
|
+
end
|
|
127
154
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
template_stmts.each_with_index do |stmt, t_idx|
|
|
143
|
-
sig = @template_analysis.generate_signature(stmt)
|
|
144
|
-
|
|
145
|
-
if sig && dest_sigs.key?(sig)
|
|
146
|
-
d_idx = dest_sigs[sig]
|
|
147
|
-
alignment << {
|
|
148
|
-
type: :match,
|
|
149
|
-
template_stmt: stmt,
|
|
150
|
-
dest_stmt: dest_stmts[d_idx],
|
|
151
|
-
template_index: t_idx,
|
|
152
|
-
dest_index: d_idx,
|
|
153
|
-
signature: sig,
|
|
154
|
-
}
|
|
155
|
-
matched_dest_indices << d_idx
|
|
155
|
+
match_key = @dest_analysis.generate_signature(dest_node)
|
|
156
|
+
template_match = find_unmatched(template_index[match_key], matched_template_indices)
|
|
157
|
+
|
|
158
|
+
if template_match
|
|
159
|
+
matched_template_indices << template_match[:index]
|
|
160
|
+
consumed_indices << template_match[:index]
|
|
161
|
+
process_match(template_match[:node], dest_node)
|
|
162
|
+
flush_ready_trailing_groups(
|
|
163
|
+
trailing_groups: trailing_groups,
|
|
164
|
+
matched_indices: matched_indices,
|
|
165
|
+
consumed_indices: consumed_indices
|
|
166
|
+
) do |info|
|
|
167
|
+
add_template_only_node(info[:node], info[:index])
|
|
168
|
+
end
|
|
156
169
|
else
|
|
157
|
-
|
|
158
|
-
type: :template_only,
|
|
159
|
-
template_stmt: stmt,
|
|
160
|
-
template_index: t_idx,
|
|
161
|
-
signature: sig,
|
|
162
|
-
}
|
|
170
|
+
process_dest_only(dest_node, dest_nodes.index(dest_node))
|
|
163
171
|
end
|
|
164
172
|
end
|
|
165
173
|
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
type: :dest_only,
|
|
172
|
-
dest_stmt: stmt,
|
|
173
|
-
dest_index: d_idx,
|
|
174
|
-
signature: @dest_analysis.generate_signature(stmt),
|
|
175
|
-
}
|
|
174
|
+
emit_remaining_trailing_groups(
|
|
175
|
+
trailing_groups: trailing_groups,
|
|
176
|
+
consumed_indices: consumed_indices
|
|
177
|
+
) do |info|
|
|
178
|
+
add_template_only_node(info[:node], info[:index])
|
|
176
179
|
end
|
|
177
180
|
|
|
178
|
-
|
|
179
|
-
|
|
181
|
+
emit_root_boundary(:postlude)
|
|
182
|
+
|
|
183
|
+
merged_content = @result.to_s
|
|
184
|
+
healed_content = collapse_cross_source_preamble_prefixes(merged_content)
|
|
185
|
+
update_result_content(@result, healed_content) if healed_content != merged_content
|
|
186
|
+
|
|
187
|
+
@result
|
|
180
188
|
end
|
|
181
189
|
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
map[sig] ||= idx if sig
|
|
192
|
-
end
|
|
193
|
-
map
|
|
194
|
-
end
|
|
195
|
-
|
|
196
|
-
# Sort alignment entries for output
|
|
197
|
-
# @param alignment [Array<Hash>] Alignment entries
|
|
198
|
-
# @param dest_size [Integer] Number of destination statements
|
|
199
|
-
# @return [Array<Hash>]
|
|
200
|
-
def sort_alignment(alignment, dest_size)
|
|
201
|
-
alignment.sort_by do |entry|
|
|
202
|
-
case entry[:type]
|
|
203
|
-
when :match
|
|
204
|
-
# Matches: use destination position
|
|
205
|
-
[entry[:dest_index], 0]
|
|
206
|
-
when :dest_only
|
|
207
|
-
# Destination-only: use destination position
|
|
208
|
-
[entry[:dest_index], 0]
|
|
209
|
-
when :template_only
|
|
210
|
-
# Template-only: add at end, in template order
|
|
211
|
-
[dest_size + entry[:template_index], 1]
|
|
212
|
-
end
|
|
190
|
+
private
|
|
191
|
+
|
|
192
|
+
def build_match_index(nodes, analysis)
|
|
193
|
+
index = Hash.new { |h, k| h[k] = [] }
|
|
194
|
+
nodes.each_with_index do |node, idx|
|
|
195
|
+
next if freeze_node?(node)
|
|
196
|
+
|
|
197
|
+
key = analysis.generate_signature(node)
|
|
198
|
+
index[key] << { node: node, index: idx }
|
|
213
199
|
end
|
|
200
|
+
index
|
|
214
201
|
end
|
|
215
202
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
case entry[:type]
|
|
222
|
-
when :match
|
|
223
|
-
process_match(entry)
|
|
224
|
-
when :template_only
|
|
225
|
-
process_template_only(entry)
|
|
226
|
-
when :dest_only
|
|
227
|
-
process_dest_only(entry)
|
|
228
|
-
end
|
|
203
|
+
def destination_signature_set(nodes, analysis)
|
|
204
|
+
nodes.each_with_object(Set.new) do |node, signatures|
|
|
205
|
+
next if freeze_node?(node)
|
|
206
|
+
|
|
207
|
+
signatures << analysis.generate_signature(node)
|
|
229
208
|
end
|
|
230
209
|
end
|
|
231
210
|
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
# @return [void]
|
|
235
|
-
def process_match(entry)
|
|
236
|
-
dest_stmt = entry[:dest_stmt]
|
|
211
|
+
def find_unmatched(entries, matched_indices)
|
|
212
|
+
return unless entries
|
|
237
213
|
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
214
|
+
entries.find { |entry| !matched_indices.include?(entry[:index]) }
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
def trailing_group_node_matched?(node, _signature)
|
|
218
|
+
freeze_node?(node)
|
|
219
|
+
end
|
|
243
220
|
|
|
244
|
-
|
|
245
|
-
resolved_pref = resolve_preference(
|
|
221
|
+
def process_match(template_stmt, dest_stmt)
|
|
222
|
+
resolved_pref = resolve_preference(template_stmt, dest_stmt)
|
|
246
223
|
|
|
247
224
|
case resolved_pref
|
|
248
225
|
when :template
|
|
249
|
-
|
|
250
|
-
when :destination
|
|
251
|
-
@result.add_from_destination(entry[:dest_index], decision: MergeResult::DECISION_DESTINATION)
|
|
226
|
+
emit_template_preferred_match(template_stmt, dest_stmt)
|
|
252
227
|
else
|
|
253
|
-
|
|
228
|
+
add_retained_leading_gap(dest_stmt, @dest_analysis, decision: MergeResult::DECISION_DESTINATION)
|
|
229
|
+
@result.add_raw(node_lines_for(dest_stmt, @dest_analysis), decision: MergeResult::DECISION_DESTINATION)
|
|
254
230
|
end
|
|
255
231
|
end
|
|
256
232
|
|
|
@@ -288,37 +264,284 @@ module Dotenv
|
|
|
288
264
|
return stmt unless stmt
|
|
289
265
|
|
|
290
266
|
# Check by class name
|
|
291
|
-
type_key = stmt.class.name&.split(
|
|
267
|
+
type_key = stmt.class.name&.split('::')&.last
|
|
292
268
|
callable = @node_typing[type_key] || @node_typing[type_key&.to_sym]
|
|
293
269
|
return callable.call(stmt) if callable
|
|
294
270
|
|
|
295
271
|
stmt
|
|
296
272
|
end
|
|
297
273
|
|
|
298
|
-
|
|
299
|
-
# @param entry [Hash] Alignment entry
|
|
300
|
-
# @return [void]
|
|
301
|
-
def process_template_only(entry)
|
|
274
|
+
def add_template_only_node(stmt, _index)
|
|
302
275
|
return unless @add_template_only_nodes
|
|
276
|
+
return if freeze_node?(stmt)
|
|
277
|
+
|
|
278
|
+
# Intentional product behavior: template-only dotenv additions do not
|
|
279
|
+
# import surrounding template comment context. The matrix marks those
|
|
280
|
+
# comment-bearing template-only cases as out of scope unless this policy
|
|
281
|
+
# is changed deliberately.
|
|
282
|
+
@result.add_raw(
|
|
283
|
+
node_lines_for(
|
|
284
|
+
stmt,
|
|
285
|
+
@template_analysis,
|
|
286
|
+
include_leading_segment: false,
|
|
287
|
+
include_interstitial_trailing_segment: false
|
|
288
|
+
),
|
|
289
|
+
decision: MergeResult::DECISION_ADDED
|
|
290
|
+
)
|
|
291
|
+
end
|
|
303
292
|
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
293
|
+
def process_dest_only(dest_stmt, _dest_index)
|
|
294
|
+
if remove_destination_only_assignment?(dest_stmt)
|
|
295
|
+
lines = removed_destination_comment_lines_for(dest_stmt)
|
|
296
|
+
@result.add_raw(lines, decision: MergeResult::DECISION_DESTINATION) if lines.any?
|
|
297
|
+
else
|
|
298
|
+
@result.add_raw(node_lines_for(dest_stmt, @dest_analysis), decision: MergeResult::DECISION_DESTINATION)
|
|
299
|
+
end
|
|
300
|
+
end
|
|
307
301
|
|
|
308
|
-
|
|
302
|
+
def emit_template_preferred_match(template_stmt, dest_stmt)
|
|
303
|
+
comment_source_node, comment_source_analysis = preferred_comment_source_for(template_stmt, dest_stmt)
|
|
304
|
+
inline_comment = preferred_inline_comment_for(template_stmt, dest_stmt)
|
|
305
|
+
add_retained_leading_gap(dest_stmt, @dest_analysis, decision: MergeResult::DECISION_DESTINATION)
|
|
306
|
+
@result.add_raw(
|
|
307
|
+
node_lines_for(
|
|
308
|
+
template_stmt,
|
|
309
|
+
@template_analysis,
|
|
310
|
+
comment_source_node: comment_source_node,
|
|
311
|
+
comment_source_analysis: comment_source_analysis,
|
|
312
|
+
inline_comment: inline_comment
|
|
313
|
+
),
|
|
314
|
+
decision: MergeResult::DECISION_TEMPLATE
|
|
315
|
+
)
|
|
309
316
|
end
|
|
310
317
|
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
def process_dest_only(entry)
|
|
315
|
-
dest_stmt = entry[:dest_stmt]
|
|
318
|
+
def preferred_comment_source_for(template_stmt, dest_stmt)
|
|
319
|
+
return [dest_stmt, @dest_analysis] if node_has_leading_comments?(dest_stmt, @dest_analysis)
|
|
320
|
+
return [template_stmt, @template_analysis] if node_has_leading_comments?(template_stmt, @template_analysis)
|
|
316
321
|
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
[template_stmt, @template_analysis]
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
def node_has_leading_comments?(node, analysis)
|
|
326
|
+
leading_segment_lines_for(node, analysis).any? { |line| !line.to_s.strip.empty? }
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
def preferred_inline_comment_for(template_stmt, dest_stmt)
|
|
330
|
+
return unless preserve_destination_inline_comment_for_template_match?(template_stmt, dest_stmt)
|
|
331
|
+
|
|
332
|
+
destination_inline_comment_for(dest_stmt)
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
def preserve_destination_inline_comment_for_template_match?(template_stmt, dest_stmt)
|
|
336
|
+
return false unless template_stmt.is_a?(EnvLine) && dest_stmt.is_a?(EnvLine)
|
|
337
|
+
return false unless template_stmt.assignment? && dest_stmt.assignment?
|
|
338
|
+
return false unless destination_inline_comment_for(dest_stmt)
|
|
339
|
+
|
|
340
|
+
template_inline_comment_for(template_stmt).nil?
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
def remove_destination_only_assignment?(stmt)
|
|
344
|
+
@remove_template_missing_nodes && stmt.is_a?(EnvLine) && stmt.assignment?
|
|
345
|
+
end
|
|
346
|
+
|
|
347
|
+
def destination_inline_comment_for(stmt)
|
|
348
|
+
@dest_analysis.comment_tracker.inline_comment_at(stmt.line_number)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
def template_inline_comment_for(stmt)
|
|
352
|
+
@template_analysis.comment_tracker.inline_comment_at(stmt.line_number)
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
def freeze_node?(node)
|
|
356
|
+
node.is_a?(FreezeNode) || (node.respond_to?(:is_a?) && node.is_a?(Ast::Merge::Freezable))
|
|
357
|
+
end
|
|
358
|
+
|
|
359
|
+
def emit_root_boundary(kind)
|
|
360
|
+
lines = root_boundary_lines_for(kind, @dest_analysis)
|
|
361
|
+
return if lines.empty?
|
|
362
|
+
|
|
363
|
+
decision = MergeResult::DECISION_DESTINATION
|
|
364
|
+
@result.add_raw(lines, decision: decision)
|
|
365
|
+
end
|
|
366
|
+
|
|
367
|
+
def root_boundary_lines_for(kind, analysis)
|
|
368
|
+
super(kind, analysis, owners: analysis.structural_owners, fallback_to_owner_bounds: true)
|
|
369
|
+
end
|
|
370
|
+
|
|
371
|
+
def emission_start_line_for(node, analysis)
|
|
372
|
+
region_start = region_start_line(leading_region_for(node, analysis))
|
|
373
|
+
preamble_start = analysis.comment_augmenter.preamble_region&.start_line if first_structural_owner?(node,
|
|
374
|
+
analysis)
|
|
375
|
+
preceding_blank_line_start(region_start || preamble_start || node.start_line, analysis)
|
|
376
|
+
end
|
|
377
|
+
|
|
378
|
+
def root_boundary_owner_start_line_for(node, analysis)
|
|
379
|
+
emission_start_line_for(node, analysis)
|
|
380
|
+
end
|
|
381
|
+
|
|
382
|
+
def first_structural_owner?(node, analysis)
|
|
383
|
+
Array(analysis.structural_owners).first.equal?(node)
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
def node_lines_for(
|
|
387
|
+
node,
|
|
388
|
+
analysis,
|
|
389
|
+
comment_source_node: node,
|
|
390
|
+
comment_source_analysis: analysis,
|
|
391
|
+
inline_comment: nil,
|
|
392
|
+
include_leading_segment: true,
|
|
393
|
+
include_interstitial_trailing_segment: true
|
|
394
|
+
)
|
|
395
|
+
return freeze_block_lines_for(node) if freeze_node?(node)
|
|
396
|
+
|
|
397
|
+
leading_lines = if include_leading_segment
|
|
398
|
+
leading_segment_lines_for(comment_source_node,
|
|
399
|
+
comment_source_analysis)
|
|
400
|
+
else
|
|
401
|
+
[]
|
|
402
|
+
end
|
|
403
|
+
node_lines = (node.start_line..node.end_line).filter_map { |line_number| raw_line_at(analysis, line_number) }
|
|
404
|
+
trailing_lines = if include_interstitial_trailing_segment
|
|
405
|
+
interstitial_trailing_segment_lines_for(node,
|
|
406
|
+
analysis)
|
|
407
|
+
else
|
|
408
|
+
[]
|
|
409
|
+
end
|
|
410
|
+
leading_lines + apply_inline_comment(node_lines, inline_comment) + trailing_lines
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
def add_retained_leading_gap(node, analysis, decision:)
|
|
414
|
+
lines = retained_owner_leading_gap_lines_for(node, analysis, owners: analysis.structural_owners)
|
|
415
|
+
return if lines.empty?
|
|
416
|
+
|
|
417
|
+
@result.add_raw(lines, decision: decision)
|
|
418
|
+
end
|
|
419
|
+
|
|
420
|
+
def removed_destination_comment_lines_for(node)
|
|
421
|
+
inline_lines = []
|
|
422
|
+
|
|
423
|
+
if (inline_comment = destination_inline_comment_for(node))
|
|
424
|
+
inline_lines << promoted_inline_comment_line_for(node, @dest_analysis, inline_comment)
|
|
321
425
|
end
|
|
426
|
+
|
|
427
|
+
removed_owner_preserved_lines_for(
|
|
428
|
+
node,
|
|
429
|
+
@dest_analysis,
|
|
430
|
+
owners: @dest_analysis.structural_owners,
|
|
431
|
+
inline_lines: inline_lines
|
|
432
|
+
)
|
|
433
|
+
end
|
|
434
|
+
|
|
435
|
+
def interstitial_trailing_segment_lines_for(node, analysis)
|
|
436
|
+
interstitial_trailing_region_lines_for(node, analysis, owners: analysis.structural_owners)
|
|
437
|
+
end
|
|
438
|
+
|
|
439
|
+
def next_structural_owner_for(node, analysis)
|
|
440
|
+
owners = Array(analysis.structural_owners)
|
|
441
|
+
index = owners.index(node)
|
|
442
|
+
return unless index
|
|
443
|
+
|
|
444
|
+
owners[index + 1]
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
def apply_inline_comment(lines, inline_comment)
|
|
448
|
+
return lines if inline_comment.nil? || lines.empty?
|
|
449
|
+
|
|
450
|
+
updated_lines = lines.dup
|
|
451
|
+
updated_lines[-1] = "#{updated_lines[-1].rstrip} #{inline_comment[:raw].sub(/\A\s+/, '')}"
|
|
452
|
+
updated_lines
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
def promoted_inline_comment_line_for(node, analysis, inline_comment)
|
|
456
|
+
raw_line = raw_line_at(analysis, node.start_line)
|
|
457
|
+
return unless raw_line
|
|
458
|
+
|
|
459
|
+
"#{raw_line[/\A\s*/]}#{inline_comment[:raw].sub(/\A\s+/, '')}"
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
def freeze_block_lines_for(node)
|
|
463
|
+
node.lines.map { |line| line.respond_to?(:raw) ? line.raw : line.to_s }
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
STANDALONE_DOTENV_COMMENT_LINE_RE = /\A\s*#.*\z/
|
|
467
|
+
private_constant :STANDALONE_DOTENV_COMMENT_LINE_RE
|
|
468
|
+
|
|
469
|
+
def collapse_cross_source_preamble_prefixes(content)
|
|
470
|
+
template_comments, = leading_standalone_comment_run(@template_content.to_s)
|
|
471
|
+
return content if template_comments.empty?
|
|
472
|
+
|
|
473
|
+
merged_comments, remainder = leading_standalone_comment_run(content)
|
|
474
|
+
return content if merged_comments.empty?
|
|
475
|
+
|
|
476
|
+
template_tally = template_comments.tally
|
|
477
|
+
merged_tally = merged_comments.tally
|
|
478
|
+
duplicated_template_prefix = template_tally.any? do |line, count|
|
|
479
|
+
merged_tally.fetch(line, 0) > count
|
|
480
|
+
end
|
|
481
|
+
return content unless duplicated_template_prefix
|
|
482
|
+
|
|
483
|
+
destination_specific_comments = merged_comments.reject { |line| template_comments.include?(line) }
|
|
484
|
+
return content if destination_specific_comments.empty?
|
|
485
|
+
|
|
486
|
+
should_heal = ::Ast::Merge::Healer.handle(
|
|
487
|
+
mode: @corruption_handling,
|
|
488
|
+
kind: :duplicate_template_preamble_prefix,
|
|
489
|
+
message: 'merged dotenv preamble begins with duplicated template-owned comment lines',
|
|
490
|
+
prefix: '[dotenv-merge]',
|
|
491
|
+
error_class: Dotenv::Merge::CorruptionDetectedError,
|
|
492
|
+
warner: lambda { |formatted|
|
|
493
|
+
DebugLogger.debug_warning(formatted, {
|
|
494
|
+
template_comment_lines: template_comments.length,
|
|
495
|
+
merged_comment_lines: merged_comments.length,
|
|
496
|
+
destination_specific_comment_lines: destination_specific_comments.length
|
|
497
|
+
})
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
return content unless should_heal
|
|
501
|
+
|
|
502
|
+
remainder = remainder.sub(/\A(?:\s*\n)+/, '')
|
|
503
|
+
rebuilt = destination_specific_comments.join("\n")
|
|
504
|
+
return rebuilt if remainder.empty?
|
|
505
|
+
|
|
506
|
+
"#{rebuilt}\n\n#{remainder}"
|
|
507
|
+
end
|
|
508
|
+
|
|
509
|
+
def leading_standalone_comment_run(text)
|
|
510
|
+
lines = text.to_s.split("\n", -1)
|
|
511
|
+
comment_lines = []
|
|
512
|
+
index = 0
|
|
513
|
+
|
|
514
|
+
while index < lines.length
|
|
515
|
+
line = lines[index]
|
|
516
|
+
if line.strip.empty?
|
|
517
|
+
comment_lines << line if comment_lines.any?
|
|
518
|
+
index += 1
|
|
519
|
+
next
|
|
520
|
+
end
|
|
521
|
+
|
|
522
|
+
break unless STANDALONE_DOTENV_COMMENT_LINE_RE.match?(line)
|
|
523
|
+
|
|
524
|
+
comment_lines << line
|
|
525
|
+
index += 1
|
|
526
|
+
end
|
|
527
|
+
|
|
528
|
+
[comment_lines, lines.drop(index).join("\n")]
|
|
529
|
+
end
|
|
530
|
+
|
|
531
|
+
def raw_line_at(analysis, line_number)
|
|
532
|
+
line = analysis.line_at(line_number)
|
|
533
|
+
line.respond_to?(:raw) ? line.raw : line
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
def leading_segment_anchor_line_for(node, analysis, owners: nil, leading_region: nil)
|
|
537
|
+
region_start = super(node, analysis, owners: owners || analysis.structural_owners, leading_region: leading_region)
|
|
538
|
+
preamble_start = analysis.comment_augmenter.preamble_region&.start_line if first_structural_owner?(node,
|
|
539
|
+
analysis)
|
|
540
|
+
region_start || preamble_start
|
|
541
|
+
end
|
|
542
|
+
|
|
543
|
+
def source_line_at(analysis, line_number)
|
|
544
|
+
raw_line_at(analysis, line_number)
|
|
322
545
|
end
|
|
323
546
|
end
|
|
324
547
|
end
|
data/lib/dotenv/merge/version.rb
CHANGED
|
@@ -2,11 +2,12 @@
|
|
|
2
2
|
|
|
3
3
|
module Dotenv
|
|
4
4
|
module Merge
|
|
5
|
-
# Version
|
|
5
|
+
# Version namespace for this gem.
|
|
6
6
|
module Version
|
|
7
|
-
# Current version
|
|
8
|
-
VERSION =
|
|
7
|
+
# Current gem version.
|
|
8
|
+
VERSION = '7.1.1'
|
|
9
9
|
end
|
|
10
|
-
|
|
10
|
+
# Current gem version exposed at the traditional constant location.
|
|
11
|
+
VERSION = Version::VERSION # Traditional Constant Location
|
|
11
12
|
end
|
|
12
13
|
end
|
data/lib/dotenv/merge.rb
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
# External gems
|
|
4
|
-
require "version_gem"
|
|
5
|
-
require "set"
|
|
6
4
|
|
|
7
5
|
# Shared merge infrastructure
|
|
8
|
-
require
|
|
6
|
+
require 'ast/merge'
|
|
7
|
+
require 'plain/merge'
|
|
8
|
+
require 'tree_haver'
|
|
9
|
+
require_relative 'merge/version'
|
|
9
10
|
|
|
10
11
|
# This gem
|
|
11
|
-
require_relative "merge/version"
|
|
12
12
|
|
|
13
13
|
module Dotenv
|
|
14
14
|
module Merge
|
|
@@ -59,27 +59,31 @@ module Dotenv
|
|
|
59
59
|
# end
|
|
60
60
|
class DestinationParseError < ParseError; end
|
|
61
61
|
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
autoload :
|
|
66
|
-
autoload :
|
|
67
|
-
autoload :
|
|
62
|
+
# Raised when merge-time corruption detection is configured to error.
|
|
63
|
+
class CorruptionDetectedError < Error; end
|
|
64
|
+
|
|
65
|
+
autoload :DebugLogger, 'dotenv/merge/debug_logger'
|
|
66
|
+
autoload :Backend, 'dotenv/merge/backend'
|
|
67
|
+
autoload :CommentTracker, 'dotenv/merge/comment_tracker'
|
|
68
|
+
autoload :EnvLine, 'dotenv/merge/env_line'
|
|
69
|
+
autoload :FreezeNode, 'dotenv/merge/freeze_node'
|
|
70
|
+
autoload :FileAnalysis, 'dotenv/merge/file_analysis'
|
|
71
|
+
autoload :MergeResult, 'dotenv/merge/merge_result'
|
|
72
|
+
autoload :SmartMerger, 'dotenv/merge/smart_merger'
|
|
68
73
|
end
|
|
69
74
|
end
|
|
70
75
|
|
|
76
|
+
require_relative 'merge/backend'
|
|
77
|
+
Dotenv::Merge.register_backend!
|
|
78
|
+
|
|
71
79
|
# Register with ast-merge's MergeGemRegistry for RSpec dependency tags
|
|
72
80
|
# Only register if MergeGemRegistry is loaded (i.e., in test environment)
|
|
73
81
|
if defined?(Ast::Merge::RSpec::MergeGemRegistry)
|
|
74
82
|
Ast::Merge::RSpec::MergeGemRegistry.register(
|
|
75
83
|
:dotenv_merge,
|
|
76
|
-
require_path:
|
|
77
|
-
merger_class:
|
|
78
|
-
test_source:
|
|
79
|
-
category: :config
|
|
84
|
+
require_path: 'dotenv/merge',
|
|
85
|
+
merger_class: 'Dotenv::Merge::SmartMerger',
|
|
86
|
+
test_source: 'KEY=value',
|
|
87
|
+
category: :config
|
|
80
88
|
)
|
|
81
89
|
end
|
|
82
|
-
|
|
83
|
-
Dotenv::Merge::Version.class_eval do
|
|
84
|
-
extend VersionGem::Basic
|
|
85
|
-
end
|