prism-merge 1.0.3 → 1.1.0
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/CHANGELOG.md +34 -1
- data/README.md +2 -1
- data/lib/prism/merge/conflict_resolver.rb +58 -26
- data/lib/prism/merge/debug_logger.rb +54 -0
- data/lib/prism/merge/file_aligner.rb +12 -11
- data/lib/prism/merge/file_analysis.rb +263 -192
- data/lib/prism/merge/freeze_node.rb +161 -0
- data/lib/prism/merge/smart_merger.rb +216 -6
- data/lib/prism/merge/version.rb +1 -1
- data/lib/prism/merge.rb +2 -0
- data.tar.gz.sig +0 -0
- metadata +6 -4
- 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: edeb460ce4645e2e6a8caf390cca9b23abeb88df9b3c314f6f86b42bc4ed68f5
|
|
4
|
+
data.tar.gz: 2f2fb56d28d1d8ddb2563b8b312186f06bd96b26bdf89753e1382724faac9332
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 1a5fa5900b24ba063df2de14c8772fda7b4dcebacfd2529dc41b465550b2baad3f8b8597d2c9759eac69b4025218843c35a41c14273c4ce69da294469b09b166
|
|
7
|
+
data.tar.gz: ac7e0ba8364617dcc2e9c67955b7c5cf303028c54cfaee272eb86a4f26378bd7c0eeed585db714b851bdbd42aae710b24a1fe9a6d629d880d0e26482e222cd9d
|
checksums.yaml.gz.sig
CHANGED
|
Binary file
|
data/CHANGELOG.md
CHANGED
|
@@ -30,6 +30,37 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
30
30
|
|
|
31
31
|
### Security
|
|
32
32
|
|
|
33
|
+
## [1.1.0] - 2025-12-04
|
|
34
|
+
|
|
35
|
+
- TAG: [v1.1.0][1.1.0t]
|
|
36
|
+
- COVERAGE: 95.65% -- 770/805 lines in 9 files
|
|
37
|
+
- BRANCH COVERAGE: 81.13% -- 245/302 branches in 9 files
|
|
38
|
+
- 100.00% documented
|
|
39
|
+
|
|
40
|
+
### Added
|
|
41
|
+
|
|
42
|
+
- Recursive merge support for class and module bodies - nested structures are now merged intelligently
|
|
43
|
+
- Conditional signature matching for `if`/`unless` blocks based on condition expression
|
|
44
|
+
- Freeze block validation for partial/incomplete nodes and freeze blocks inside non-class/module contexts
|
|
45
|
+
- Freeze blocks now match by position/order when both files have multiple freeze blocks
|
|
46
|
+
- `add_template_only_nodes` option now properly respected in recursive merges and boundary processing
|
|
47
|
+
- `DebugLogger`, controlled by `ENV["PRISM_MERGE_DEBUG"]` set to true or false
|
|
48
|
+
- more specs
|
|
49
|
+
|
|
50
|
+
### Changed
|
|
51
|
+
|
|
52
|
+
- Migrated to Prism v1.6.0 native comment attachment (removed custom comment association logic)
|
|
53
|
+
- Simplified FileAnalysis implementation using Prism's built-in features
|
|
54
|
+
- Improved node lookup to handle anchors with leading comments (e.g., magic comments)
|
|
55
|
+
|
|
56
|
+
### Fixed
|
|
57
|
+
|
|
58
|
+
- Template-only nodes are now correctly excluded in all contexts when `add_template_only_nodes: false`
|
|
59
|
+
- Freeze blocks inside methods now properly raise InvalidStructureError (only class/module-level freeze blocks allowed)
|
|
60
|
+
- Freeze block matching now works correctly with multiple consecutive freeze blocks (matches by index/order)
|
|
61
|
+
- Duplicate freeze blocks from template no longer appear when destination has matching freeze blocks
|
|
62
|
+
- Magic comments at file top no longer prevent node lookup in recursive merges
|
|
63
|
+
|
|
33
64
|
## [1.0.3] - 2025-12-03
|
|
34
65
|
|
|
35
66
|
- TAG: [v1.0.3][1.0.3t]
|
|
@@ -85,7 +116,9 @@ Please file a bug if you notice a violation of semantic versioning.
|
|
|
85
116
|
|
|
86
117
|
- Initial release
|
|
87
118
|
|
|
88
|
-
[Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.0
|
|
119
|
+
[Unreleased]: https://github.com/kettle-rb/prism-merge/compare/v1.1.0...HEAD
|
|
120
|
+
[1.1.0]: https://github.com/kettle-rb/prism-merge/compare/v1.0.3...v1.1.0
|
|
121
|
+
[1.1.0t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.1.0
|
|
89
122
|
[1.0.3]: https://github.com/kettle-rb/prism-merge/compare/v1.0.2...v1.0.3
|
|
90
123
|
[1.0.3t]: https://github.com/kettle-rb/prism-merge/releases/tag/v1.0.3
|
|
91
124
|
[1.0.2]: https://github.com/kettle-rb/prism-merge/compare/v1.0.1...v1.0.2
|
data/README.md
CHANGED
|
@@ -60,6 +60,7 @@ Prism::Merge is a standalone Ruby module that intelligently merges two versions
|
|
|
60
60
|
|
|
61
61
|
- **AST-Aware**: Uses Prism parser to understand Ruby structure
|
|
62
62
|
- **Intelligent**: Matches nodes by structural signatures
|
|
63
|
+
- **Recursive Merge**: Automatically merges class and module bodies recursively, intelligently combining nested methods and constants
|
|
63
64
|
- **Comment-Preserving**: Comments are properly attached to relevant nodes and/or placement
|
|
64
65
|
- **Freeze Block Support**: Respects `kettle-dev:freeze` markers for template merge control
|
|
65
66
|
- **Full Provenance**: Tracks origin of every line
|
|
@@ -972,7 +973,7 @@ Thanks for RTFM. ☺️
|
|
|
972
973
|
[📌gitmoji]: https://gitmoji.dev
|
|
973
974
|
[📌gitmoji-img]: https://img.shields.io/badge/gitmoji_commits-%20%F0%9F%98%9C%20%F0%9F%98%8D-34495e.svg?style=flat-square
|
|
974
975
|
[🧮kloc]: https://www.youtube.com/watch?v=dQw4w9WgXcQ
|
|
975
|
-
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.
|
|
976
|
+
[🧮kloc-img]: https://img.shields.io/badge/KLOC-0.805-FFDD67.svg?style=for-the-badge&logo=YouTube&logoColor=blue
|
|
976
977
|
[🔐security]: SECURITY.md
|
|
977
978
|
[🔐security-img]: https://img.shields.io/badge/security-policy-259D6C.svg?style=flat
|
|
978
979
|
[📄copyright-notice-explainer]: https://opensource.stackexchange.com/questions/5778/why-do-licenses-such-as-the-mit-license-specify-a-single-year
|
|
@@ -91,7 +91,10 @@ module Prism
|
|
|
91
91
|
end
|
|
92
92
|
|
|
93
93
|
if dest_content[:lines].empty?
|
|
94
|
-
|
|
94
|
+
# Only add template-only content if the flag allows it
|
|
95
|
+
if @add_template_only_nodes
|
|
96
|
+
add_content_to_result(template_content, result, :template, MergeResult::DECISION_KEPT_TEMPLATE)
|
|
97
|
+
end
|
|
95
98
|
return
|
|
96
99
|
end
|
|
97
100
|
|
|
@@ -143,8 +146,8 @@ module Prism
|
|
|
143
146
|
end
|
|
144
147
|
|
|
145
148
|
def merge_boundary_content(template_content, dest_content, _boundary, result)
|
|
146
|
-
# Strategy: Process
|
|
147
|
-
#
|
|
149
|
+
# Strategy: Process nodes in order using signature matching.
|
|
150
|
+
# FreezeNodes from destination are always preferred to preserve customizations.
|
|
148
151
|
|
|
149
152
|
template_nodes = template_content[:nodes]
|
|
150
153
|
dest_nodes = dest_content[:nodes]
|
|
@@ -152,25 +155,34 @@ module Prism
|
|
|
152
155
|
# Track which dest nodes have been matched
|
|
153
156
|
matched_dest_indices = Set.new
|
|
154
157
|
|
|
155
|
-
#
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
158
|
+
# Check if there are any FreezeNodes in destination - they always win
|
|
159
|
+
dest_freeze_nodes = dest_nodes.select { |n| n[:node].is_a?(FreezeNode) }
|
|
160
|
+
|
|
161
|
+
if dest_freeze_nodes.any?
|
|
162
|
+
# Add all destination freeze blocks as-is
|
|
163
|
+
dest_freeze_nodes.each do |freeze_node_info|
|
|
164
|
+
result.add_node(
|
|
165
|
+
freeze_node_info,
|
|
166
|
+
decision: MergeResult::DECISION_FREEZE_BLOCK,
|
|
167
|
+
source: :destination,
|
|
168
|
+
source_analysis: @dest_analysis,
|
|
169
|
+
)
|
|
170
|
+
matched_dest_indices << freeze_node_info[:index]
|
|
171
|
+
|
|
172
|
+
# Mark any template nodes within this freeze block range as processed
|
|
173
|
+
freeze_range = freeze_node_info[:line_range]
|
|
174
|
+
template_nodes.each do |t_node_info|
|
|
175
|
+
if freeze_range.cover?(t_node_info[:line_range].begin) &&
|
|
176
|
+
freeze_range.cover?(t_node_info[:line_range].end)
|
|
177
|
+
# Template node is inside freeze block, skip it
|
|
178
|
+
# (we'll handle this by checking if it overlaps with a freeze block)
|
|
167
179
|
end
|
|
168
180
|
end
|
|
169
181
|
end
|
|
170
182
|
end
|
|
171
183
|
|
|
172
|
-
# Build signature map for destination nodes
|
|
173
|
-
dest_sig_map = build_signature_map(dest_nodes)
|
|
184
|
+
# Build signature map for destination nodes (excluding already-matched freeze nodes)
|
|
185
|
+
dest_sig_map = build_signature_map(dest_nodes.reject { |n| matched_dest_indices.include?(n[:index]) })
|
|
174
186
|
|
|
175
187
|
# Build a set of line numbers that are covered by leading comments of nodes
|
|
176
188
|
# so we don't duplicate them when processing non-node lines
|
|
@@ -188,7 +200,6 @@ module Prism
|
|
|
188
200
|
current_line = template_line_range.begin
|
|
189
201
|
# Track if we're in a sequence of template-only nodes
|
|
190
202
|
in_template_only_sequence = false
|
|
191
|
-
last_added_line = nil
|
|
192
203
|
|
|
193
204
|
sorted_nodes = template_nodes.sort_by { |n| n[:line_range].begin }
|
|
194
205
|
|
|
@@ -196,6 +207,18 @@ module Prism
|
|
|
196
207
|
node_start = t_node_info[:line_range].begin
|
|
197
208
|
node_end = t_node_info[:line_range].end
|
|
198
209
|
|
|
210
|
+
# Skip template nodes that overlap with destination freeze blocks
|
|
211
|
+
overlaps_freeze = dest_freeze_nodes.any? do |freeze_info|
|
|
212
|
+
freeze_range = freeze_info[:line_range]
|
|
213
|
+
node_start.between?(freeze_range.begin, freeze_range.end) ||
|
|
214
|
+
node_end.between?(freeze_range.begin, freeze_range.end)
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
if overlaps_freeze
|
|
218
|
+
current_line = node_end + 1
|
|
219
|
+
next
|
|
220
|
+
end
|
|
221
|
+
|
|
199
222
|
# Check if this node will be matched or is template-only
|
|
200
223
|
sig = t_node_info[:signature]
|
|
201
224
|
is_matched = dest_sig_map[sig]&.any?
|
|
@@ -213,11 +236,9 @@ module Prism
|
|
|
213
236
|
trailing_blank_end = line_num
|
|
214
237
|
end
|
|
215
238
|
|
|
216
|
-
node_start..trailing_blank_end
|
|
217
|
-
|
|
218
239
|
# Add any non-node, non-blank lines before this node (e.g., comments not attached to nodes)
|
|
219
|
-
if in_template_only_sequence && !is_matched
|
|
220
|
-
# Skip lines before template-only nodes in a sequence
|
|
240
|
+
if (in_template_only_sequence && !is_matched) || (!is_matched && !@add_template_only_nodes)
|
|
241
|
+
# Skip lines before template-only nodes in a sequence OR when add_template_only_nodes is false
|
|
221
242
|
current_line = node_start
|
|
222
243
|
else
|
|
223
244
|
while current_line < node_start
|
|
@@ -296,7 +317,6 @@ module Prism
|
|
|
296
317
|
end
|
|
297
318
|
|
|
298
319
|
in_template_only_sequence = false
|
|
299
|
-
last_added_line = trailing_blank_end
|
|
300
320
|
elsif @add_template_only_nodes
|
|
301
321
|
# No match - this is a template-only node
|
|
302
322
|
result.add_node(
|
|
@@ -313,7 +333,6 @@ module Prism
|
|
|
313
333
|
end
|
|
314
334
|
|
|
315
335
|
in_template_only_sequence = false
|
|
316
|
-
last_added_line = trailing_blank_end
|
|
317
336
|
# Add the template-only node
|
|
318
337
|
else
|
|
319
338
|
# Skip template-only nodes (don't add template nodes that don't exist in destination)
|
|
@@ -422,12 +441,25 @@ module Prism
|
|
|
422
441
|
end
|
|
423
442
|
|
|
424
443
|
def handle_orphan_lines(template_content, dest_content, result)
|
|
444
|
+
# With CommentNodes integrated into statements, there should be far fewer orphan lines
|
|
445
|
+
# Orphan lines are now only truly standalone content like blank lines or
|
|
446
|
+
# inline content not attached to nodes.
|
|
447
|
+
|
|
425
448
|
# Find lines that aren't part of any node (pure comments, blank lines)
|
|
426
449
|
template_orphans = find_orphan_lines(@template_analysis, template_content[:line_range], template_content[:nodes])
|
|
427
450
|
dest_orphans = find_orphan_lines(@dest_analysis, dest_content[:line_range], dest_content[:nodes])
|
|
428
451
|
|
|
429
|
-
#
|
|
430
|
-
|
|
452
|
+
# Add template orphans first
|
|
453
|
+
template_orphans.each do |line_num|
|
|
454
|
+
line = @template_analysis.line_at(line_num)
|
|
455
|
+
result.add_line(
|
|
456
|
+
line.chomp,
|
|
457
|
+
decision: MergeResult::DECISION_KEPT_TEMPLATE,
|
|
458
|
+
template_line: line_num,
|
|
459
|
+
)
|
|
460
|
+
end
|
|
461
|
+
|
|
462
|
+
# Then add unique destination orphans (ones not in template)
|
|
431
463
|
template_orphan_content = Set.new(template_orphans.map { |ln| @template_analysis.normalized_line(ln) })
|
|
432
464
|
|
|
433
465
|
dest_orphans.each do |line_num|
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Prism
|
|
4
|
+
module Merge
|
|
5
|
+
# Internal debug logging utility.
|
|
6
|
+
# Only logs when PRISM_MERGE_DEBUG environment variable is set.
|
|
7
|
+
# Optionally uses Ruby's Logger if available, otherwise falls back to simple puts.
|
|
8
|
+
# rubocop:disable ThreadSafety/ClassInstanceVariable
|
|
9
|
+
module DebugLogger
|
|
10
|
+
@logger = nil
|
|
11
|
+
@enabled = ENV.fetch("PRISM_MERGE_DEBUG", "false").casecmp?("true")
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_reader :enabled
|
|
15
|
+
|
|
16
|
+
# Log a debug message if debugging is enabled
|
|
17
|
+
# @param message [String] The message to log
|
|
18
|
+
# @param context [Hash] Optional context information
|
|
19
|
+
def debug(message, context = {})
|
|
20
|
+
return unless @enabled
|
|
21
|
+
|
|
22
|
+
if logger_available?
|
|
23
|
+
ensure_logger
|
|
24
|
+
context_str = context.empty? ? "" : " #{context.inspect}"
|
|
25
|
+
@logger.debug("[prism-merge] #{message}#{context_str}")
|
|
26
|
+
else
|
|
27
|
+
context_str = context.empty? ? "" : " | #{context.inspect}"
|
|
28
|
+
puts "[DEBUG][prism-merge] #{message}#{context_str}"
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
private
|
|
33
|
+
|
|
34
|
+
# Check if Logger is available without raising an error
|
|
35
|
+
def logger_available?
|
|
36
|
+
defined?(Logger)
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
# Initialize logger if not already done
|
|
40
|
+
def ensure_logger
|
|
41
|
+
return if @logger
|
|
42
|
+
|
|
43
|
+
require "logger"
|
|
44
|
+
@logger = Logger.new($stdout)
|
|
45
|
+
@logger.level = Logger::DEBUG
|
|
46
|
+
rescue LoadError
|
|
47
|
+
# Logger not available, will fall back to puts
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
# rubocop:enable ThreadSafety/ClassInstanceVariable
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -283,20 +283,21 @@ module Prism
|
|
|
283
283
|
|
|
284
284
|
def add_freeze_block_anchors
|
|
285
285
|
# Freeze blocks in destination should always be preserved as anchors
|
|
286
|
-
|
|
287
|
-
|
|
286
|
+
# Match freeze blocks by their index/order in the file
|
|
287
|
+
@dest_analysis.freeze_blocks.each_with_index do |freeze_node, index|
|
|
288
|
+
line_range = freeze_node.start_line..freeze_node.end_line
|
|
288
289
|
|
|
289
|
-
# Check if there's a corresponding freeze block in template
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
290
|
+
# Check if there's a corresponding freeze block in template at the same index
|
|
291
|
+
template_freeze = @template_analysis.freeze_blocks[index]
|
|
292
|
+
|
|
293
|
+
if template_freeze
|
|
294
|
+
template_range = template_freeze.start_line..template_freeze.end_line
|
|
293
295
|
|
|
294
|
-
if template_block
|
|
295
296
|
# Check if there's already an anchor covering this range
|
|
296
297
|
# (from exact line matches)
|
|
297
298
|
existing_anchor = @anchors.find do |a|
|
|
298
|
-
a.template_start <=
|
|
299
|
-
a.template_end >=
|
|
299
|
+
a.template_start <= template_range.begin &&
|
|
300
|
+
a.template_end >= template_range.end &&
|
|
300
301
|
a.dest_start <= line_range.begin &&
|
|
301
302
|
a.dest_end >= line_range.end
|
|
302
303
|
end
|
|
@@ -305,8 +306,8 @@ module Prism
|
|
|
305
306
|
unless existing_anchor
|
|
306
307
|
# Both files have this freeze block - create anchor
|
|
307
308
|
@anchors << Anchor.new(
|
|
308
|
-
|
|
309
|
-
|
|
309
|
+
template_range.begin,
|
|
310
|
+
template_range.end,
|
|
310
311
|
line_range.begin,
|
|
311
312
|
line_range.end,
|
|
312
313
|
:freeze_block,
|