listen 0.7.3 → 1.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.
- checksums.yaml +7 -0
- data/CHANGELOG.md +70 -35
- data/README.md +93 -76
- data/lib/listen.rb +34 -19
- data/lib/listen/adapter.rb +179 -81
- data/lib/listen/adapters/bsd.rb +27 -64
- data/lib/listen/adapters/darwin.rb +21 -58
- data/lib/listen/adapters/linux.rb +23 -55
- data/lib/listen/adapters/polling.rb +25 -34
- data/lib/listen/adapters/windows.rb +50 -46
- data/lib/listen/directory_record.rb +88 -58
- data/lib/listen/listener.rb +111 -37
- data/lib/listen/multi_listener.rb +5 -133
- data/lib/listen/turnstile.rb +9 -5
- data/lib/listen/version.rb +1 -1
- metadata +69 -22
- data/lib/listen/dependency_manager.rb +0 -126
@@ -11,8 +11,10 @@ module Listen
|
|
11
11
|
class DirectoryRecord
|
12
12
|
attr_reader :directory, :paths, :sha1_checksums
|
13
13
|
|
14
|
+
# The default list of directories that get ignored by the listener.
|
14
15
|
DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
|
15
16
|
|
17
|
+
# The default list of files that get ignored by the listener.
|
16
18
|
DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
|
17
19
|
|
18
20
|
# Defines the used precision based on the type of mtime returned by the
|
@@ -56,15 +58,14 @@ module Listen
|
|
56
58
|
def initialize(directory)
|
57
59
|
raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
|
58
60
|
|
59
|
-
@directory
|
60
|
-
@ignoring_patterns
|
61
|
-
@filtering_patterns = Set.new
|
62
|
-
@sha1_checksums = Hash.new
|
61
|
+
@directory, @sha1_checksums = directory, Hash.new
|
62
|
+
@ignoring_patterns, @filtering_patterns = Set.new, Set.new
|
63
63
|
|
64
64
|
@ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
|
65
65
|
end
|
66
66
|
|
67
|
-
# Returns the ignoring patterns in the record
|
67
|
+
# Returns the ignoring patterns in the record to know
|
68
|
+
# which paths should be ignored.
|
68
69
|
#
|
69
70
|
# @return [Array<Regexp>] the ignoring patterns
|
70
71
|
#
|
@@ -72,7 +73,7 @@ module Listen
|
|
72
73
|
@ignoring_patterns.to_a
|
73
74
|
end
|
74
75
|
|
75
|
-
# Returns the filtering patterns
|
76
|
+
# Returns the filtering patterns in the record to know
|
76
77
|
# which paths should be stored.
|
77
78
|
#
|
78
79
|
# @return [Array<Regexp>] the filtering patterns
|
@@ -86,10 +87,10 @@ module Listen
|
|
86
87
|
# @example Ignore some paths
|
87
88
|
# ignore %r{^ignored/path/}, /man/
|
88
89
|
#
|
89
|
-
# @param [Regexp]
|
90
|
+
# @param [Regexp] regexps a list of patterns for ignoring paths
|
90
91
|
#
|
91
92
|
def ignore(*regexps)
|
92
|
-
@ignoring_patterns.merge(regexps)
|
93
|
+
@ignoring_patterns.merge(regexps).reject! { |r| r.nil? }
|
93
94
|
end
|
94
95
|
|
95
96
|
# Replaces ignoring patterns in the record.
|
@@ -97,37 +98,37 @@ module Listen
|
|
97
98
|
# @example Ignore only these paths
|
98
99
|
# ignore! %r{^ignored/path/}, /man/
|
99
100
|
#
|
100
|
-
# @param [Regexp]
|
101
|
+
# @param [Regexp] regexps a list of patterns for ignoring paths
|
101
102
|
#
|
102
103
|
def ignore!(*regexps)
|
103
|
-
@ignoring_patterns.replace(regexps)
|
104
|
+
@ignoring_patterns.replace(regexps).reject! { |r| r.nil? }
|
104
105
|
end
|
105
106
|
|
106
|
-
# Adds filtering patterns to the
|
107
|
+
# Adds filtering patterns to the record.
|
107
108
|
#
|
108
109
|
# @example Filter some files
|
109
|
-
#
|
110
|
+
# filter /\.txt$/, /.*\.zip/
|
110
111
|
#
|
111
|
-
# @param [Regexp]
|
112
|
+
# @param [Regexp] regexps a list of patterns for filtering files
|
112
113
|
#
|
113
114
|
def filter(*regexps)
|
114
|
-
@filtering_patterns.merge(regexps)
|
115
|
+
@filtering_patterns.merge(regexps).reject! { |r| r.nil? }
|
115
116
|
end
|
116
117
|
|
117
|
-
# Replaces filtering patterns in the
|
118
|
+
# Replaces filtering patterns in the record.
|
118
119
|
#
|
119
120
|
# @example Filter only these files
|
120
|
-
#
|
121
|
+
# filter! /\.txt$/, /.*\.zip/
|
121
122
|
#
|
122
|
-
# @param [Regexp]
|
123
|
+
# @param [Regexp] regexps a list of patterns for filtering files
|
123
124
|
#
|
124
125
|
def filter!(*regexps)
|
125
|
-
@filtering_patterns.replace(regexps)
|
126
|
+
@filtering_patterns.replace(regexps).reject! { |r| r.nil? }
|
126
127
|
end
|
127
128
|
|
128
129
|
# Returns whether a path should be ignored or not.
|
129
130
|
#
|
130
|
-
# @param [String] path the path to test
|
131
|
+
# @param [String] path the path to test
|
131
132
|
#
|
132
133
|
# @return [Boolean]
|
133
134
|
#
|
@@ -138,7 +139,7 @@ module Listen
|
|
138
139
|
|
139
140
|
# Returns whether a path should be filtered or not.
|
140
141
|
#
|
141
|
-
# @param [String] path the path to test
|
142
|
+
# @param [String] path the path to test
|
142
143
|
#
|
143
144
|
# @return [Boolean]
|
144
145
|
#
|
@@ -159,9 +160,9 @@ module Listen
|
|
159
160
|
end
|
160
161
|
|
161
162
|
# Detects changes in the passed directories, updates
|
162
|
-
# the record with the new changes and returns the changes
|
163
|
+
# the record with the new changes and returns the changes.
|
163
164
|
#
|
164
|
-
# @param [Array] directories the list of directories scan for changes
|
165
|
+
# @param [Array] directories the list of directories to scan for changes
|
165
166
|
# @param [Hash] options
|
166
167
|
# @option options [Boolean] recursive scan all sub-directories recursively
|
167
168
|
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
@@ -174,6 +175,7 @@ module Listen
|
|
174
175
|
|
175
176
|
directories.each do |directory|
|
176
177
|
next unless directory[@directory] # Path is or inside directory
|
178
|
+
|
177
179
|
detect_modifications_and_removals(directory, options)
|
178
180
|
detect_additions(directory, options)
|
179
181
|
end
|
@@ -188,9 +190,10 @@ module Listen
|
|
188
190
|
# @return [String] the relative path
|
189
191
|
#
|
190
192
|
def relative_to_base(path)
|
191
|
-
return nil unless path[
|
193
|
+
return nil unless path[directory]
|
194
|
+
|
192
195
|
path = path.force_encoding("BINARY") if path.respond_to?(:force_encoding)
|
193
|
-
path.sub(%r{^#{Regexp.quote(
|
196
|
+
path.sub(%r{^#{Regexp.quote(directory)}#{File::SEPARATOR}?}, '')
|
194
197
|
end
|
195
198
|
|
196
199
|
private
|
@@ -207,43 +210,69 @@ module Listen
|
|
207
210
|
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
208
211
|
#
|
209
212
|
def detect_modifications_and_removals(directory, options = {})
|
210
|
-
|
213
|
+
paths[directory].each do |basename, meta_data|
|
211
214
|
path = File.join(directory, basename)
|
212
|
-
|
213
215
|
case meta_data.type
|
214
216
|
when 'Dir'
|
215
|
-
|
216
|
-
detect_modifications_and_removals(path, options) if options[:recursive]
|
217
|
-
else
|
218
|
-
detect_modifications_and_removals(path, { :recursive => true }.merge(options))
|
219
|
-
@paths[directory].delete(basename)
|
220
|
-
@paths.delete("#{directory}/#{basename}")
|
221
|
-
end
|
217
|
+
detect_modification_or_removal_for_dir(path, options)
|
222
218
|
when 'File'
|
223
|
-
|
224
|
-
|
219
|
+
detect_modification_or_removal_for_file(path, meta_data, options)
|
220
|
+
end
|
221
|
+
end
|
222
|
+
end
|
225
223
|
|
226
|
-
|
227
|
-
# before checking the time difference
|
228
|
-
if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
|
229
|
-
# Update the sha1 checksum of the file
|
230
|
-
insert_sha1_checksum(path)
|
224
|
+
def detect_modification_or_removal_for_dir(path, options)
|
231
225
|
|
232
|
-
|
233
|
-
|
234
|
-
|
226
|
+
# Directory still exists
|
227
|
+
if File.directory?(path)
|
228
|
+
detect_modifications_and_removals(path, options) if options[:recursive]
|
235
229
|
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
230
|
+
# Directory has been removed
|
231
|
+
else
|
232
|
+
detect_modifications_and_removals(path, options)
|
233
|
+
@paths[File.dirname(path)].delete(File.basename(path))
|
234
|
+
@paths.delete("#{File.dirname(path)}/#{File.basename(path)}")
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
def detect_modification_or_removal_for_file(path, meta_data, options)
|
239
|
+
# File still exists
|
240
|
+
if File.exist?(path)
|
241
|
+
detect_modification(path, meta_data, options)
|
242
|
+
|
243
|
+
# File has been removed
|
244
|
+
else
|
245
|
+
removal_detected(path, meta_data, options)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
|
249
|
+
def detect_modification(path, meta_data, options)
|
250
|
+
new_mtime = mtime_of(path)
|
251
|
+
|
252
|
+
# First check if we are in the same second (to update checksums)
|
253
|
+
# before checking the time difference
|
254
|
+
if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
|
255
|
+
modification_detected(path, meta_data, new_mtime, options)
|
244
256
|
end
|
245
257
|
end
|
246
258
|
|
259
|
+
def modification_detected(path, meta_data, new_mtime, options)
|
260
|
+
# Update the sha1 checksum of the file
|
261
|
+
update_sha1_checksum(path)
|
262
|
+
|
263
|
+
# Update the meta data of the file
|
264
|
+
meta_data.mtime = new_mtime
|
265
|
+
@paths[File.dirname(path)][File.basename(path)] = meta_data
|
266
|
+
|
267
|
+
@changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
|
268
|
+
end
|
269
|
+
|
270
|
+
def removal_detected(path, meta_data, options)
|
271
|
+
@paths[File.dirname(path)].delete(File.basename(path))
|
272
|
+
@sha1_checksums.delete(path)
|
273
|
+
@changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
|
274
|
+
end
|
275
|
+
|
247
276
|
# Detects additions in a directory.
|
248
277
|
#
|
249
278
|
# @param [String] directory the path to analyze
|
@@ -283,9 +312,10 @@ module Listen
|
|
283
312
|
# @param [String] path the file path
|
284
313
|
#
|
285
314
|
def content_modified?(path)
|
315
|
+
return false unless File.ftype(path) == 'file'
|
286
316
|
@sha1_checksum = sha1_checksum(path)
|
287
|
-
if
|
288
|
-
|
317
|
+
if sha1_checksums[path] == @sha1_checksum || !sha1_checksums.key?(path)
|
318
|
+
update_sha1_checksum(path)
|
289
319
|
false
|
290
320
|
else
|
291
321
|
true
|
@@ -296,7 +326,7 @@ module Listen
|
|
296
326
|
#
|
297
327
|
# @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
|
298
328
|
#
|
299
|
-
def
|
329
|
+
def update_sha1_checksum(path)
|
300
330
|
if @sha1_checksum ||= sha1_checksum(path)
|
301
331
|
@sha1_checksums[path] = @sha1_checksum
|
302
332
|
@sha1_checksum = nil
|
@@ -314,13 +344,13 @@ module Listen
|
|
314
344
|
end
|
315
345
|
|
316
346
|
# Traverses the base directory looking for paths that should
|
317
|
-
# be stored; thus paths that are
|
347
|
+
# be stored; thus paths that are filtered or not ignored.
|
318
348
|
#
|
319
349
|
# @yield [path] an important path
|
320
350
|
#
|
321
351
|
def important_paths
|
322
|
-
Find.find(
|
323
|
-
next if path ==
|
352
|
+
Find.find(directory) do |path|
|
353
|
+
next if path == directory
|
324
354
|
|
325
355
|
if File.directory?(path)
|
326
356
|
# Add a trailing slash to directories when checking if a directory is
|
@@ -355,7 +385,7 @@ module Listen
|
|
355
385
|
# @return [Boolean]
|
356
386
|
#
|
357
387
|
def existing_path?(path)
|
358
|
-
|
388
|
+
paths[File.dirname(path)][File.basename(path)] != nil
|
359
389
|
end
|
360
390
|
|
361
391
|
# Returns the modification time of a file based on the precision defined by the system
|
data/lib/listen/listener.rb
CHANGED
@@ -2,14 +2,16 @@ require 'pathname'
|
|
2
2
|
|
3
3
|
module Listen
|
4
4
|
class Listener
|
5
|
-
attr_reader :
|
5
|
+
attr_reader :directories, :directories_records, :block, :adapter, :adapter_options, :use_relative_paths
|
6
6
|
|
7
|
-
|
8
|
-
|
7
|
+
BLOCKING_PARAMETER_DEPRECATION_MESSAGE = <<-EOS.gsub(/^\s*/, '')
|
8
|
+
The blocking parameter of Listen::Listener#start is deprecated.\n
|
9
|
+
Please use Listen::Adapter#start for a non-blocking listener and Listen::Listener#start! for a blocking one.
|
10
|
+
EOS
|
9
11
|
|
10
|
-
# Initializes the
|
12
|
+
# Initializes the directories listener.
|
11
13
|
#
|
12
|
-
# @param [String] directory the
|
14
|
+
# @param [String] directory the directories to listen to
|
13
15
|
# @param [Hash] options the listen options
|
14
16
|
# @option options [Regexp] ignore a pattern for ignoring paths
|
15
17
|
# @option options [Regexp] filter a pattern for filtering paths
|
@@ -23,36 +25,48 @@ module Listen
|
|
23
25
|
# @yieldparam [Array<String>] added the list of added files
|
24
26
|
# @yieldparam [Array<String>] removed the list of removed files
|
25
27
|
#
|
26
|
-
def initialize(
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
28
|
+
def initialize(*args, &block)
|
29
|
+
options = args.last.is_a?(Hash) ? args.pop : {}
|
30
|
+
directories = args.flatten
|
31
|
+
initialize_directories_and_directories_records(directories)
|
32
|
+
initialize_relative_paths_usage(options)
|
33
|
+
@block = block
|
31
34
|
|
32
|
-
|
33
|
-
|
34
|
-
@directory_record.filter(*options.delete(:filter)) if options[:filter]
|
35
|
+
ignore(*options.delete(:ignore))
|
36
|
+
filter(*options.delete(:filter))
|
35
37
|
|
36
38
|
@adapter_options = options
|
37
39
|
end
|
38
40
|
|
39
41
|
# Starts the listener by initializing the adapter and building
|
40
42
|
# the directory record concurrently, then it starts the adapter to watch
|
41
|
-
# for changes.
|
43
|
+
# for changes. The current thread is not blocked after starting.
|
42
44
|
#
|
43
|
-
# @
|
45
|
+
# @see Listen::Listener#start!
|
44
46
|
#
|
45
|
-
def start(
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
47
|
+
def start(deprecated_blocking = nil)
|
48
|
+
Kernel.warn "[Listen warning]:\n#{BLOCKING_PARAMETER_DEPRECATION_MESSAGE}" unless deprecated_blocking.nil?
|
49
|
+
setup
|
50
|
+
adapter.start
|
51
|
+
end
|
52
|
+
|
53
|
+
# Starts the listener by initializing the adapter and building
|
54
|
+
# the directory record concurrently, then it starts the adapter to watch
|
55
|
+
# for changes. The current thread is blocked after starting.
|
56
|
+
#
|
57
|
+
# @see Listen::Listener#start
|
58
|
+
#
|
59
|
+
# @since 1.0.0
|
60
|
+
#
|
61
|
+
def start!
|
62
|
+
setup
|
63
|
+
adapter.start!
|
50
64
|
end
|
51
65
|
|
52
66
|
# Stops the listener.
|
53
67
|
#
|
54
68
|
def stop
|
55
|
-
|
69
|
+
adapter.stop
|
56
70
|
end
|
57
71
|
|
58
72
|
# Pauses the listener.
|
@@ -60,7 +74,7 @@ module Listen
|
|
60
74
|
# @return [Listen::Listener] the listener
|
61
75
|
#
|
62
76
|
def pause
|
63
|
-
|
77
|
+
adapter.pause
|
64
78
|
self
|
65
79
|
end
|
66
80
|
|
@@ -69,8 +83,8 @@ module Listen
|
|
69
83
|
# @return [Listen::Listener] the listener
|
70
84
|
#
|
71
85
|
def unpause
|
72
|
-
|
73
|
-
|
86
|
+
build_directories_records
|
87
|
+
adapter.unpause
|
74
88
|
self
|
75
89
|
end
|
76
90
|
|
@@ -79,7 +93,7 @@ module Listen
|
|
79
93
|
# @return [Boolean] adapter paused status
|
80
94
|
#
|
81
95
|
def paused?
|
82
|
-
|
96
|
+
!!adapter && adapter.paused?
|
83
97
|
end
|
84
98
|
|
85
99
|
# Adds ignoring patterns to the listener.
|
@@ -88,8 +102,10 @@ module Listen
|
|
88
102
|
#
|
89
103
|
# @return [Listen::Listener] the listener
|
90
104
|
#
|
105
|
+
# @see Listen::DirectoryRecord#ignore
|
106
|
+
#
|
91
107
|
def ignore(*regexps)
|
92
|
-
|
108
|
+
directories_records.each { |r| r.ignore(*regexps) }
|
93
109
|
self
|
94
110
|
end
|
95
111
|
|
@@ -99,8 +115,10 @@ module Listen
|
|
99
115
|
#
|
100
116
|
# @return [Listen::Listener] the listener
|
101
117
|
#
|
118
|
+
# @see Listen::DirectoryRecord#ignore!
|
119
|
+
#
|
102
120
|
def ignore!(*regexps)
|
103
|
-
|
121
|
+
directories_records.each { |r| r.ignore!(*regexps) }
|
104
122
|
self
|
105
123
|
end
|
106
124
|
|
@@ -110,19 +128,23 @@ module Listen
|
|
110
128
|
#
|
111
129
|
# @return [Listen::Listener] the listener
|
112
130
|
#
|
131
|
+
# @see Listen::DirectoryRecord#filter
|
132
|
+
#
|
113
133
|
def filter(*regexps)
|
114
|
-
|
134
|
+
directories_records.each { |r| r.filter(*regexps) }
|
115
135
|
self
|
116
136
|
end
|
117
137
|
|
118
|
-
#
|
138
|
+
# Replaces filtering patterns in the listener.
|
119
139
|
#
|
120
140
|
# @param (see Listen::DirectoryRecord#filter!)
|
121
141
|
#
|
122
142
|
# @return [Listen::Listener] the listener
|
123
143
|
#
|
144
|
+
# @see Listen::DirectoryRecord#filter!
|
145
|
+
#
|
124
146
|
def filter!(*regexps)
|
125
|
-
|
147
|
+
directories_records.each { |r| r.filter!(*regexps) }
|
126
148
|
self
|
127
149
|
end
|
128
150
|
|
@@ -171,7 +193,7 @@ module Listen
|
|
171
193
|
self
|
172
194
|
end
|
173
195
|
|
174
|
-
# Defines a custom polling fallback message
|
196
|
+
# Defines a custom polling fallback message or disable it.
|
175
197
|
#
|
176
198
|
# @example Disabling the polling fallback message
|
177
199
|
# polling_fallback_message false
|
@@ -204,22 +226,74 @@ module Listen
|
|
204
226
|
#
|
205
227
|
# @param (see Listen::DirectoryRecord#fetch_changes)
|
206
228
|
#
|
229
|
+
# @see Listen::DirectoryRecord#fetch_changes
|
230
|
+
#
|
207
231
|
def on_change(directories, options = {})
|
208
|
-
changes =
|
209
|
-
:relative_paths => @use_relative_paths
|
210
|
-
))
|
232
|
+
changes = fetch_records_changes(directories, options)
|
211
233
|
unless changes.values.all? { |paths| paths.empty? }
|
212
|
-
|
234
|
+
block.call(changes[:modified], changes[:added], changes[:removed])
|
213
235
|
end
|
214
236
|
end
|
215
237
|
|
216
238
|
private
|
217
239
|
|
240
|
+
# Initializes the directories to watch as well as the directories records.
|
241
|
+
#
|
242
|
+
# @see Listen::DirectoryRecord
|
243
|
+
#
|
244
|
+
def initialize_directories_and_directories_records(directories)
|
245
|
+
@directories = directories.map { |d| Pathname.new(d).realpath.to_s }
|
246
|
+
@directories_records = directories.map { |d| DirectoryRecord.new(d) }
|
247
|
+
end
|
248
|
+
|
249
|
+
# Initializes whether or not using relative paths.
|
250
|
+
#
|
251
|
+
def initialize_relative_paths_usage(options)
|
252
|
+
@use_relative_paths = directories.one? && options.delete(:relative_paths) { true }
|
253
|
+
end
|
254
|
+
|
255
|
+
# Build the directory record concurrently and initialize the adapter.
|
256
|
+
#
|
257
|
+
def setup
|
258
|
+
t = Thread.new { build_directories_records }
|
259
|
+
@adapter = initialize_adapter
|
260
|
+
t.join
|
261
|
+
end
|
262
|
+
|
218
263
|
# Initializes an adapter passing it the callback and adapters' options.
|
219
264
|
#
|
220
265
|
def initialize_adapter
|
221
|
-
callback = lambda { |
|
222
|
-
Adapter.select_and_initialize(
|
266
|
+
callback = lambda { |changed_directories, options| self.on_change(changed_directories, options) }
|
267
|
+
Adapter.select_and_initialize(directories, adapter_options, &callback)
|
268
|
+
end
|
269
|
+
|
270
|
+
# Build the watched directories' records.
|
271
|
+
#
|
272
|
+
def build_directories_records
|
273
|
+
directories_records.each { |r| r.build }
|
223
274
|
end
|
275
|
+
|
276
|
+
# Returns the sum of all the changes to the directories records
|
277
|
+
#
|
278
|
+
# @param (see Listen::DirectoryRecord#fetch_changes)
|
279
|
+
#
|
280
|
+
# @return [Hash] the changes
|
281
|
+
#
|
282
|
+
def fetch_records_changes(directories_to_search, options)
|
283
|
+
directories_records.inject({}) do |h, r|
|
284
|
+
# directory records skips paths outside their range, so passing the
|
285
|
+
# whole `directories` array is not a problem.
|
286
|
+
record_changes = r.fetch_changes(directories_to_search, options.merge(:relative_paths => use_relative_paths))
|
287
|
+
|
288
|
+
if h.empty?
|
289
|
+
h.merge!(record_changes)
|
290
|
+
else
|
291
|
+
h.each { |k, v| h[k] += record_changes[k] }
|
292
|
+
end
|
293
|
+
|
294
|
+
h
|
295
|
+
end
|
296
|
+
end
|
297
|
+
|
224
298
|
end
|
225
299
|
end
|