ast-merge 3.1.0 → 4.0.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.
@@ -0,0 +1,380 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Merge
5
+ module Navigable
6
+ # Wraps any node (parser-backed or synthetic) with uniform navigation.
7
+ #
8
+ # Provides two levels of navigation:
9
+ # 1. **Flat list navigation**: prev_statement, next_statement, index
10
+ # - Works for ALL nodes (synthetic and parser-backed)
11
+ # - Represents position in the flattened statement list
12
+ #
13
+ # 2. **Tree navigation**: tree_parent, tree_next, tree_previous, tree_children
14
+ # - Only available for parser-backed nodes
15
+ # - Delegates to inner_node's tree methods
16
+ #
17
+ # This allows code to work with the flat list for simple merging,
18
+ # while still accessing tree structure for section-aware operations.
19
+ #
20
+ # @example Basic usage
21
+ # statements = Statement.build_list(raw_statements)
22
+ # stmt = statements[0]
23
+ #
24
+ # # Flat navigation (always works)
25
+ # stmt.next # => next statement in flat list
26
+ # stmt.previous # => previous statement in flat list
27
+ # stmt.index # => position in array
28
+ #
29
+ # # Tree navigation (when available)
30
+ # stmt.tree_parent # => parent in original AST (or nil)
31
+ # stmt.tree_next # => next sibling in original AST (or nil)
32
+ # stmt.tree_children # => children in original AST (or [])
33
+ #
34
+ # @example Section grouping
35
+ # # Group statements into sections by heading level
36
+ # sections = Statement.group_by_heading(statements, level: 3)
37
+ # sections.each do |section|
38
+ # puts "Section: #{section.heading.text}"
39
+ # section.statements.each { |s| puts " - #{s.type}" }
40
+ # end
41
+ #
42
+ class Statement
43
+ # @return [Object] The wrapped node (parser-backed or synthetic)
44
+ attr_reader :node
45
+
46
+ # @return [Integer] Index in the flattened statement list
47
+ attr_reader :index
48
+
49
+ # @return [Statement, nil] Previous statement in flat list
50
+ attr_accessor :prev_statement
51
+
52
+ # @return [Statement, nil] Next statement in flat list
53
+ attr_accessor :next_statement
54
+
55
+ # @return [Object, nil] Optional context/metadata for this statement
56
+ attr_accessor :context
57
+
58
+ # Initialize a Statement wrapper.
59
+ #
60
+ # @param node [Object] The node to wrap
61
+ # @param index [Integer] Position in the statement list
62
+ def initialize(node, index:)
63
+ @node = node
64
+ @index = index
65
+ @prev_statement = nil
66
+ @next_statement = nil
67
+ @context = nil
68
+ end
69
+
70
+ class << self
71
+ # Build a linked list of Statements from raw statements.
72
+ #
73
+ # @param raw_statements [Array<Object>] Raw statement nodes
74
+ # @return [Array<Statement>] Linked statement list
75
+ def build_list(raw_statements)
76
+ statements = raw_statements.each_with_index.map do |node, i|
77
+ new(node, index: i)
78
+ end
79
+
80
+ # Link siblings in flat list
81
+ statements.each_cons(2) do |prev_stmt, next_stmt|
82
+ prev_stmt.next_statement = next_stmt
83
+ next_stmt.prev_statement = prev_stmt
84
+ end
85
+
86
+ statements
87
+ end
88
+
89
+ # Find statements matching a query.
90
+ #
91
+ # @param statements [Array<Statement>] Statement list
92
+ # @param type [Symbol, String, nil] Node type to match (nil = any)
93
+ # @param text [String, Regexp, nil] Text pattern to match
94
+ # @yield [Statement] Optional block for custom matching
95
+ # @return [Array<Statement>] Matching statements
96
+ def find_matching(statements, type: nil, text: nil, &block)
97
+ # If no criteria specified, return empty array (nothing to match)
98
+ return [] if type.nil? && text.nil? && !block_given?
99
+
100
+ statements.select do |stmt|
101
+ matches = true
102
+ matches &&= stmt.type.to_s == type.to_s if type
103
+ matches &&= text.is_a?(Regexp) ? stmt.text.match?(text) : stmt.text.include?(text.to_s) if text
104
+ matches &&= yield(stmt) if block_given?
105
+ matches
106
+ end
107
+ end
108
+
109
+ # Find the first statement matching criteria.
110
+ #
111
+ # @param statements [Array<Statement>] Statement list
112
+ # @param type [Symbol, String, nil] Node type to match
113
+ # @param text [String, Regexp, nil] Text pattern to match
114
+ # @yield [Statement] Optional block for custom matching
115
+ # @return [Statement, nil] First matching statement
116
+ def find_first(statements, type: nil, text: nil, &block)
117
+ find_matching(statements, type: type, text: text, &block).first
118
+ end
119
+ end
120
+
121
+ # ============================================================
122
+ # Flat list navigation (always available)
123
+ # ============================================================
124
+
125
+ # @return [Statement, nil] Next statement in flat list
126
+ def next
127
+ next_statement
128
+ end
129
+
130
+ # @return [Statement, nil] Previous statement in flat list
131
+ def previous
132
+ prev_statement
133
+ end
134
+
135
+ # @return [Boolean] true if this is the first statement
136
+ def first?
137
+ prev_statement.nil?
138
+ end
139
+
140
+ # @return [Boolean] true if this is the last statement
141
+ def last?
142
+ next_statement.nil?
143
+ end
144
+
145
+ # Iterate from this statement to the end (or until block returns false).
146
+ #
147
+ # @yield [Statement] Each statement
148
+ # @return [Enumerator, nil]
149
+ def each_following(&block)
150
+ return to_enum(:each_following) unless block_given?
151
+
152
+ current = self.next
153
+ while current
154
+ break unless yield(current)
155
+ current = current.next
156
+ end
157
+ end
158
+
159
+ # Collect statements until a condition is met.
160
+ #
161
+ # @yield [Statement] Each statement
162
+ # @return [Array<Statement>] Statements until condition
163
+ def take_until(&block)
164
+ result = []
165
+ each_following do |stmt|
166
+ break if yield(stmt)
167
+ result << stmt
168
+ true
169
+ end
170
+ result
171
+ end
172
+
173
+ # ============================================================
174
+ # Tree navigation (delegates to inner_node when available)
175
+ # ============================================================
176
+
177
+ # @return [Object, nil] Parent node in original AST
178
+ def tree_parent
179
+ inner = unwrapped_node
180
+ inner.parent if inner.respond_to?(:parent)
181
+ end
182
+
183
+ # @return [Object, nil] Next sibling in original AST
184
+ def tree_next
185
+ inner = unwrapped_node
186
+ inner.next if inner.respond_to?(:next)
187
+ end
188
+
189
+ # @return [Object, nil] Previous sibling in original AST
190
+ def tree_previous
191
+ inner = unwrapped_node
192
+ inner.previous if inner.respond_to?(:previous)
193
+ end
194
+
195
+ # @return [Array<Object>] Children in original AST
196
+ def tree_children
197
+ inner = unwrapped_node
198
+ if inner.respond_to?(:each)
199
+ inner.to_a
200
+ elsif inner.respond_to?(:children)
201
+ inner.children
202
+ else
203
+ []
204
+ end
205
+ end
206
+
207
+ # @return [Object, nil] First child in original AST
208
+ def tree_first_child
209
+ inner = unwrapped_node
210
+ inner.first_child if inner.respond_to?(:first_child)
211
+ end
212
+
213
+ # @return [Object, nil] Last child in original AST
214
+ def tree_last_child
215
+ inner = unwrapped_node
216
+ inner.last_child if inner.respond_to?(:last_child)
217
+ end
218
+
219
+ # @return [Boolean] true if tree navigation is available
220
+ def has_tree_navigation?
221
+ inner = unwrapped_node
222
+ inner.respond_to?(:parent) || inner.respond_to?(:next)
223
+ end
224
+
225
+ # @return [Boolean] true if this is a synthetic node (no tree navigation)
226
+ def synthetic?
227
+ !has_tree_navigation?
228
+ end
229
+
230
+ # Calculate the tree depth (distance from root).
231
+ #
232
+ # @return [Integer] Depth in tree (0 = root level)
233
+ def tree_depth
234
+ depth = 0
235
+ current = tree_parent
236
+ while current
237
+ depth += 1
238
+ # Navigate up through parents
239
+ if current.respond_to?(:parent)
240
+ current = current.parent
241
+ else
242
+ break
243
+ end
244
+ end
245
+ depth
246
+ end
247
+
248
+ # Check if this node is at same or shallower depth than another.
249
+ # Useful for determining section boundaries.
250
+ #
251
+ # @param other [Statement, Integer] Other statement or depth value
252
+ # @return [Boolean] true if this node is at same or shallower depth
253
+ def same_or_shallower_than?(other)
254
+ other_depth = other.is_a?(Integer) ? other : other.tree_depth
255
+ tree_depth <= other_depth
256
+ end
257
+
258
+ # ============================================================
259
+ # Node delegation
260
+ # ============================================================
261
+
262
+ # @return [Symbol, String] Node type
263
+ def type
264
+ return node.type if node.respond_to?(:type)
265
+
266
+ # Fallback: derive type from class name (handle anonymous classes)
267
+ class_name = node.class.name
268
+ class_name ? class_name.split("::").last : "Anonymous"
269
+ end
270
+
271
+ # @return [Array, Object, nil] Node signature for matching
272
+ def signature
273
+ node.signature if node.respond_to?(:signature)
274
+ end
275
+
276
+ # @return [String] Node text content
277
+ def text
278
+ # TreeHaver nodes (and any node conforming to the unified API) provide #text.
279
+ # No conditional fallbacks - nodes must conform to the API.
280
+ node.text.to_s
281
+ end
282
+
283
+ # @return [Hash, nil] Source position info
284
+ def source_position
285
+ node.source_position if node.respond_to?(:source_position)
286
+ end
287
+
288
+ # @return [Integer, nil] Start line number
289
+ def start_line
290
+ pos = source_position
291
+ pos[:start_line] if pos
292
+ end
293
+
294
+ # @return [Integer, nil] End line number
295
+ def end_line
296
+ pos = source_position
297
+ pos[:end_line] if pos
298
+ end
299
+
300
+ # ============================================================
301
+ # Node attribute helpers (language-agnostic)
302
+ # ============================================================
303
+
304
+ # Check if this node matches a type.
305
+ #
306
+ # @param expected_type [Symbol, String] Type to check
307
+ # @return [Boolean]
308
+ def type?(expected_type)
309
+ type.to_s == expected_type.to_s
310
+ end
311
+
312
+ # Check if this node's text matches a pattern.
313
+ #
314
+ # @param pattern [String, Regexp] Pattern to match
315
+ # @return [Boolean]
316
+ def text_matches?(pattern)
317
+ case pattern
318
+ when Regexp
319
+ text.match?(pattern)
320
+ else
321
+ text.include?(pattern.to_s)
322
+ end
323
+ end
324
+
325
+ # Get an attribute from the underlying node.
326
+ #
327
+ # Tries multiple method names to support different parser APIs.
328
+ #
329
+ # @param name [Symbol, String] Attribute name
330
+ # @param aliases [Array<Symbol>] Alternative method names
331
+ # @return [Object, nil] Attribute value
332
+ def node_attribute(name, *aliases)
333
+ inner = unwrapped_node
334
+ [name, *aliases].each do |method_name|
335
+ return inner.send(method_name) if inner.respond_to?(method_name)
336
+ end
337
+ nil
338
+ end
339
+
340
+ # ============================================================
341
+ # Utilities
342
+ # ============================================================
343
+
344
+ # Get the unwrapped inner node.
345
+ #
346
+ # @return [Object] The innermost node
347
+ def unwrapped_node
348
+ current = node
349
+ while current.respond_to?(:inner_node) && current.inner_node != current
350
+ current = current.inner_node
351
+ end
352
+ current
353
+ end
354
+
355
+ # @return [String] Human-readable representation
356
+ def inspect
357
+ "#<Navigable::Statement[#{index}] type=#{type} tree=#{has_tree_navigation?}>"
358
+ end
359
+
360
+ # @return [String] String representation
361
+ def to_s
362
+ text.to_s.strip[0, 50]
363
+ end
364
+
365
+ # Delegate unknown methods to the wrapped node.
366
+ def method_missing(method, *args, &block)
367
+ if node.respond_to?(method)
368
+ node.send(method, *args, &block)
369
+ else
370
+ super
371
+ end
372
+ end
373
+
374
+ def respond_to_missing?(method, include_private = false)
375
+ node.respond_to?(method, include_private) || super
376
+ end
377
+ end
378
+ end
379
+ end
380
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Ast
4
+ module Merge
5
+ # Namespace for navigation-related classes.
6
+ #
7
+ # Provides unified navigation over AST nodes regardless of the underlying parser.
8
+ # Classes in this namespace work together to enable finding and manipulating
9
+ # positions in document structures.
10
+ #
11
+ # @see Navigable::Statement Wraps nodes with navigation capabilities
12
+ # @see Navigable::InjectionPoint Represents a location for content injection
13
+ # @see Navigable::InjectionPointFinder Finds injection points by matching rules
14
+ module Navigable
15
+ autoload :Statement, "ast/merge/navigable/statement"
16
+ autoload :InjectionPoint, "ast/merge/navigable/injection_point"
17
+ autoload :InjectionPointFinder, "ast/merge/navigable/injection_point_finder"
18
+ end
19
+ end
20
+ end
@@ -129,15 +129,16 @@ module Ast
129
129
  def merge
130
130
  # Parse destination and find injection point
131
131
  d_analysis = create_analysis(destination)
132
- d_statements = NavigableStatement.build_list(d_analysis.statements)
132
+ d_statements = Navigable::Statement.build_list(d_analysis.statements)
133
133
 
134
- finder = InjectionPointFinder.new(d_statements)
134
+ finder = Navigable::InjectionPointFinder.new(d_statements)
135
135
  injection_point = finder.find(
136
136
  type: anchor[:type],
137
137
  text: anchor[:text],
138
138
  position: :replace,
139
139
  boundary_type: boundary&.dig(:type),
140
140
  boundary_text: boundary&.dig(:text),
141
+ boundary_same_or_shallower: boundary&.dig(:same_or_shallower) || false,
141
142
  )
142
143
 
143
144
  if injection_point.nil?
@@ -200,6 +201,7 @@ module Ast
200
201
  result[:level] = matcher[:level] if matcher[:level]
201
202
  result[:level_lte] = matcher[:level_lte] if matcher[:level_lte]
202
203
  result[:level_gte] = matcher[:level_gte] if matcher[:level_gte]
204
+ result[:same_or_shallower] = matcher[:same_or_shallower] if matcher.key?(:same_or_shallower)
203
205
  result.compact
204
206
  end
205
207
 
@@ -135,6 +135,20 @@ module Ast
135
135
  script_loader.load_callable(value)
136
136
  end
137
137
 
138
+ # Get the normalize_whitespace setting.
139
+ #
140
+ # @return [Boolean] Whether to collapse excessive blank lines
141
+ def normalize_whitespace
142
+ merge_config[:normalize_whitespace] == true
143
+ end
144
+
145
+ # Get the rehydrate_link_references setting.
146
+ #
147
+ # @return [Boolean] Whether to convert inline links to reference style
148
+ def rehydrate_link_references
149
+ merge_config[:rehydrate_link_references] == true
150
+ end
151
+
138
152
  # Convert preset to a hash suitable for SmartMerger options.
139
153
  #
140
154
  # @return [Hash]
@@ -146,6 +160,8 @@ module Ast
146
160
  node_typing: node_typing,
147
161
  match_refiner: match_refiner,
148
162
  freeze_token: freeze_token,
163
+ normalize_whitespace: normalize_whitespace,
164
+ rehydrate_link_references: rehydrate_link_references,
149
165
  }.compact
150
166
  end
151
167
 
@@ -168,6 +184,8 @@ module Ast
168
184
  signature_generator: config["signature_generator"],
169
185
  node_typing: config["node_typing"],
170
186
  match_refiner: config["match_refiner"],
187
+ normalize_whitespace: config["normalize_whitespace"] == true,
188
+ rehydrate_link_references: config["rehydrate_link_references"] == true,
171
189
  }
172
190
  end
173
191
 
@@ -26,7 +26,7 @@ module Ast
26
26
  #
27
27
  class Runner
28
28
  # Result of processing a single file
29
- Result = Struct.new(:path, :relative_path, :status, :changed, :has_anchor, :message, :stats, :error, keyword_init: true)
29
+ Result = Struct.new(:path, :relative_path, :status, :changed, :has_anchor, :message, :stats, :problems, :error, keyword_init: true)
30
30
 
31
31
  # @return [Config] The recipe being executed
32
32
  attr_reader :recipe
@@ -157,6 +157,8 @@ module Ast
157
157
  signature_generator: recipe.signature_generator,
158
158
  node_typing: recipe.node_typing,
159
159
  match_refiner: recipe.match_refiner,
160
+ normalize_whitespace: recipe.normalize_whitespace,
161
+ rehydrate_link_references: recipe.rehydrate_link_references,
160
162
  )
161
163
 
162
164
  result = merger.merge
@@ -202,6 +204,9 @@ module Ast
202
204
  def create_result_from_merge(target_path, relative_path, _destination_content, merge_result)
203
205
  changed = merge_result.changed
204
206
 
207
+ # Extract problems from stats if present (PartialTemplateMerger stores them there)
208
+ problems = merge_result.stats[:problems] if merge_result.stats.is_a?(Hash)
209
+
205
210
  if changed
206
211
  unless dry_run
207
212
  File.write(target_path, merge_result.content)
@@ -215,6 +220,7 @@ module Ast
215
220
  has_anchor: true,
216
221
  message: dry_run ? "Would update" : "Updated",
217
222
  stats: merge_result.stats,
223
+ problems: problems,
218
224
  )
219
225
  else
220
226
  Result.new(
@@ -225,6 +231,7 @@ module Ast
225
231
  has_anchor: true,
226
232
  message: "No changes needed",
227
233
  stats: merge_result.stats,
234
+ problems: problems,
228
235
  )
229
236
  end
230
237
  end
@@ -5,7 +5,7 @@ module Ast
5
5
  # Version information for Ast::Merge
6
6
  module Version
7
7
  # Current version of the ast-merge gem
8
- VERSION = "3.1.0"
8
+ VERSION = "4.0.0"
9
9
  end
10
10
  VERSION = Version::VERSION # traditional location
11
11
  end
data/lib/ast/merge.rb CHANGED
@@ -142,17 +142,16 @@ module Ast
142
142
  autoload :ConflictResolverBase, "ast/merge/conflict_resolver_base"
143
143
  autoload :ContentMatchRefiner, "ast/merge/content_match_refiner"
144
144
  autoload :DebugLogger, "ast/merge/debug_logger"
145
+ autoload :DiffMapperBase, "ast/merge/diff_mapper_base"
145
146
  autoload :EmitterBase, "ast/merge/emitter_base"
146
147
  autoload :FileAnalyzable, "ast/merge/file_analyzable"
147
148
  autoload :Freezable, "ast/merge/freezable"
148
149
  autoload :FreezeNodeBase, "ast/merge/freeze_node_base"
149
- autoload :InjectionPoint, "ast/merge/navigable_statement"
150
- autoload :InjectionPointFinder, "ast/merge/navigable_statement"
151
150
  autoload :MatchRefinerBase, "ast/merge/match_refiner_base"
152
151
  autoload :MatchScoreBase, "ast/merge/match_score_base"
153
152
  autoload :MergeResultBase, "ast/merge/merge_result_base"
154
153
  autoload :MergerConfig, "ast/merge/merger_config"
155
- autoload :NavigableStatement, "ast/merge/navigable_statement"
154
+ autoload :Navigable, "ast/merge/navigable"
156
155
  autoload :NodeTyping, "ast/merge/node_typing"
157
156
  autoload :NodeWrapperBase, "ast/merge/node_wrapper_base"
158
157
  autoload :PartialTemplateMergerBase, "ast/merge/partial_template_merger_base"
data.tar.gz.sig CHANGED
Binary file
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: ast-merge
3
3
  version: !ruby/object:Gem::Version
4
- version: 3.1.0
4
+ version: 4.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Peter H. Boling
@@ -63,20 +63,20 @@ dependencies:
63
63
  requirements:
64
64
  - - "~>"
65
65
  - !ruby/object:Gem::Version
66
- version: '4.0'
66
+ version: '5.0'
67
67
  - - ">="
68
68
  - !ruby/object:Gem::Version
69
- version: 4.0.0
69
+ version: 5.0.0
70
70
  type: :runtime
71
71
  prerelease: false
72
72
  version_requirements: !ruby/object:Gem::Requirement
73
73
  requirements:
74
74
  - - "~>"
75
75
  - !ruby/object:Gem::Version
76
- version: '4.0'
76
+ version: '5.0'
77
77
  - - ">="
78
78
  - !ruby/object:Gem::Version
79
- version: 4.0.0
79
+ version: 5.0.0
80
80
  - !ruby/object:Gem::Dependency
81
81
  name: kettle-dev
82
82
  requirement: !ruby/object:Gem::Requirement
@@ -261,6 +261,26 @@ dependencies:
261
261
  - - ">="
262
262
  - !ruby/object:Gem::Version
263
263
  version: 1.0.3
264
+ - !ruby/object:Gem::Dependency
265
+ name: ostruct
266
+ requirement: !ruby/object:Gem::Requirement
267
+ requirements:
268
+ - - "~>"
269
+ - !ruby/object:Gem::Version
270
+ version: '0.6'
271
+ - - ">="
272
+ - !ruby/object:Gem::Version
273
+ version: 0.6.3
274
+ type: :development
275
+ prerelease: false
276
+ version_requirements: !ruby/object:Gem::Requirement
277
+ requirements:
278
+ - - "~>"
279
+ - !ruby/object:Gem::Version
280
+ version: '0.6'
281
+ - - ">="
282
+ - !ruby/object:Gem::Version
283
+ version: 0.6.3
264
284
  description: "☯️ Ast::Merge provides base classes, modules, and RSpec shared examples
265
285
  for building intelligent file mergers using AST analysis. It powers prism-merge,
266
286
  psych-merge, json-merge, and other format-specific merge gems."
@@ -309,6 +329,7 @@ files:
309
329
  - lib/ast/merge/detector/mergeable.rb
310
330
  - lib/ast/merge/detector/toml_frontmatter.rb
311
331
  - lib/ast/merge/detector/yaml_frontmatter.rb
332
+ - lib/ast/merge/diff_mapper_base.rb
312
333
  - lib/ast/merge/emitter_base.rb
313
334
  - lib/ast/merge/file_analyzable.rb
314
335
  - lib/ast/merge/freezable.rb
@@ -317,7 +338,10 @@ files:
317
338
  - lib/ast/merge/match_score_base.rb
318
339
  - lib/ast/merge/merge_result_base.rb
319
340
  - lib/ast/merge/merger_config.rb
320
- - lib/ast/merge/navigable_statement.rb
341
+ - lib/ast/merge/navigable.rb
342
+ - lib/ast/merge/navigable/injection_point.rb
343
+ - lib/ast/merge/navigable/injection_point_finder.rb
344
+ - lib/ast/merge/navigable/statement.rb
321
345
  - lib/ast/merge/node_typing.rb
322
346
  - lib/ast/merge/node_typing/frozen_wrapper.rb
323
347
  - lib/ast/merge/node_typing/normalizer.rb
@@ -357,10 +381,10 @@ licenses:
357
381
  - MIT
358
382
  metadata:
359
383
  homepage_uri: https://ast-merge.galtzo.com/
360
- source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v3.1.0
361
- changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v3.1.0/CHANGELOG.md
384
+ source_code_uri: https://github.com/kettle-rb/ast-merge/tree/v4.0.0
385
+ changelog_uri: https://github.com/kettle-rb/ast-merge/blob/v4.0.0/CHANGELOG.md
362
386
  bug_tracker_uri: https://github.com/kettle-rb/ast-merge/issues
363
- documentation_uri: https://www.rubydoc.info/gems/ast-merge/3.1.0
387
+ documentation_uri: https://www.rubydoc.info/gems/ast-merge/4.0.0
364
388
  funding_uri: https://github.com/sponsors/pboling
365
389
  wiki_uri: https://github.com/kettle-rb/ast-merge/wiki
366
390
  news_uri: https://www.railsbling.com/tags/ast-merge
metadata.gz.sig CHANGED
Binary file