guard 0.8.0 → 0.8.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.
- data/CHANGELOG.md +310 -303
- data/LICENSE +19 -19
- data/README.md +434 -434
- data/bin/guard +6 -6
- data/lib/guard.rb +384 -384
- data/lib/guard/cli.rb +178 -179
- data/lib/guard/dsl.rb +370 -370
- data/lib/guard/dsl_describer.rb +60 -60
- data/lib/guard/group.rb +22 -22
- data/lib/guard/guard.rb +98 -98
- data/lib/guard/hook.rb +118 -118
- data/lib/guard/interactor.rb +78 -78
- data/lib/guard/listener.rb +346 -346
- data/lib/guard/listeners/darwin.rb +66 -66
- data/lib/guard/listeners/linux.rb +98 -98
- data/lib/guard/listeners/polling.rb +55 -55
- data/lib/guard/listeners/windows.rb +61 -61
- data/lib/guard/notifier.rb +211 -211
- data/lib/guard/templates/Guardfile +2 -2
- data/lib/guard/ui.rb +188 -188
- data/lib/guard/version.rb +6 -6
- data/lib/guard/watcher.rb +110 -110
- data/man/guard.1 +93 -93
- data/man/guard.1.html +176 -176
- metadata +15 -15
data/lib/guard/interactor.rb
CHANGED
@@ -1,78 +1,78 @@
|
|
1
|
-
module Guard
|
2
|
-
|
3
|
-
# The interactor reads user input and triggers
|
4
|
-
# specific action upon them unless its locked.
|
5
|
-
#
|
6
|
-
# Currently the following actions are implemented:
|
7
|
-
#
|
8
|
-
# - stop, quit, exit, s, q, e => Exit Guard
|
9
|
-
# - reload, r, z => Reload Guard
|
10
|
-
# - pause, p => Pause Guard
|
11
|
-
# - Everything else => Run all
|
12
|
-
#
|
13
|
-
class Interactor
|
14
|
-
|
15
|
-
class LockException < Exception; end
|
16
|
-
class UnlockException < Exception; end
|
17
|
-
|
18
|
-
attr_reader :locked
|
19
|
-
|
20
|
-
# Initialize the interactor in unlocked state.
|
21
|
-
#
|
22
|
-
def initialize
|
23
|
-
@locked = false
|
24
|
-
end
|
25
|
-
|
26
|
-
# Start the interactor in its own thread.
|
27
|
-
#
|
28
|
-
def start
|
29
|
-
return if ENV["GUARD_ENV"] == 'test'
|
30
|
-
|
31
|
-
@thread = Thread.new do
|
32
|
-
loop do
|
33
|
-
begin
|
34
|
-
if !@locked && (entry = $stdin.gets)
|
35
|
-
entry.gsub! /\n/, ''
|
36
|
-
case entry
|
37
|
-
when 'stop', 'quit', 'exit', 's', 'q', 'e'
|
38
|
-
::Guard.stop
|
39
|
-
when 'reload', 'r', 'z'
|
40
|
-
::Guard::Dsl.reevaluate_guardfile
|
41
|
-
::Guard.reload
|
42
|
-
when 'pause', 'p'
|
43
|
-
::Guard.pause
|
44
|
-
else
|
45
|
-
::Guard.run_all
|
46
|
-
end
|
47
|
-
end
|
48
|
-
rescue LockException
|
49
|
-
lock
|
50
|
-
rescue UnlockException
|
51
|
-
unlock
|
52
|
-
end
|
53
|
-
end
|
54
|
-
end
|
55
|
-
end
|
56
|
-
|
57
|
-
# Lock the interactor.
|
58
|
-
#
|
59
|
-
def lock
|
60
|
-
if !@thread || @thread == Thread.current
|
61
|
-
@locked = true
|
62
|
-
else
|
63
|
-
@thread.raise(LockException)
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
# Unlock the interactor.
|
68
|
-
#
|
69
|
-
def unlock
|
70
|
-
if !@thread || @thread == Thread.current
|
71
|
-
@locked = false
|
72
|
-
else
|
73
|
-
@thread.raise(UnlockException)
|
74
|
-
end
|
75
|
-
end
|
76
|
-
|
77
|
-
end
|
78
|
-
end
|
1
|
+
module Guard
|
2
|
+
|
3
|
+
# The interactor reads user input and triggers
|
4
|
+
# specific action upon them unless its locked.
|
5
|
+
#
|
6
|
+
# Currently the following actions are implemented:
|
7
|
+
#
|
8
|
+
# - stop, quit, exit, s, q, e => Exit Guard
|
9
|
+
# - reload, r, z => Reload Guard
|
10
|
+
# - pause, p => Pause Guard
|
11
|
+
# - Everything else => Run all
|
12
|
+
#
|
13
|
+
class Interactor
|
14
|
+
|
15
|
+
class LockException < Exception; end
|
16
|
+
class UnlockException < Exception; end
|
17
|
+
|
18
|
+
attr_reader :locked
|
19
|
+
|
20
|
+
# Initialize the interactor in unlocked state.
|
21
|
+
#
|
22
|
+
def initialize
|
23
|
+
@locked = false
|
24
|
+
end
|
25
|
+
|
26
|
+
# Start the interactor in its own thread.
|
27
|
+
#
|
28
|
+
def start
|
29
|
+
return if ENV["GUARD_ENV"] == 'test'
|
30
|
+
|
31
|
+
@thread = Thread.new do
|
32
|
+
loop do
|
33
|
+
begin
|
34
|
+
if !@locked && (entry = $stdin.gets)
|
35
|
+
entry.gsub! /\n/, ''
|
36
|
+
case entry
|
37
|
+
when 'stop', 'quit', 'exit', 's', 'q', 'e'
|
38
|
+
::Guard.stop
|
39
|
+
when 'reload', 'r', 'z'
|
40
|
+
::Guard::Dsl.reevaluate_guardfile
|
41
|
+
::Guard.reload
|
42
|
+
when 'pause', 'p'
|
43
|
+
::Guard.pause
|
44
|
+
else
|
45
|
+
::Guard.run_all
|
46
|
+
end
|
47
|
+
end
|
48
|
+
rescue LockException
|
49
|
+
lock
|
50
|
+
rescue UnlockException
|
51
|
+
unlock
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Lock the interactor.
|
58
|
+
#
|
59
|
+
def lock
|
60
|
+
if !@thread || @thread == Thread.current
|
61
|
+
@locked = true
|
62
|
+
else
|
63
|
+
@thread.raise(LockException)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
# Unlock the interactor.
|
68
|
+
#
|
69
|
+
def unlock
|
70
|
+
if !@thread || @thread == Thread.current
|
71
|
+
@locked = false
|
72
|
+
else
|
73
|
+
@thread.raise(UnlockException)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
|
77
|
+
end
|
78
|
+
end
|
data/lib/guard/listener.rb
CHANGED
@@ -1,346 +1,346 @@
|
|
1
|
-
require 'rbconfig'
|
2
|
-
require 'digest/sha1'
|
3
|
-
|
4
|
-
module Guard
|
5
|
-
|
6
|
-
autoload :Darwin, 'guard/listeners/darwin'
|
7
|
-
autoload :Linux, 'guard/listeners/linux'
|
8
|
-
autoload :Windows, 'guard/listeners/windows'
|
9
|
-
autoload :Polling, 'guard/listeners/polling'
|
10
|
-
|
11
|
-
# The Listener is the base class for all listener
|
12
|
-
# implementations.
|
13
|
-
#
|
14
|
-
# @abstract
|
15
|
-
#
|
16
|
-
class Listener
|
17
|
-
|
18
|
-
# Default paths that gets ignored by the listener
|
19
|
-
DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor]
|
20
|
-
|
21
|
-
attr_accessor :changed_files
|
22
|
-
attr_reader :directory, :ignore_paths, :locked
|
23
|
-
|
24
|
-
# Select the appropriate listener implementation for the
|
25
|
-
# current OS and initializes it.
|
26
|
-
#
|
27
|
-
# @param [Array] args the arguments for the listener
|
28
|
-
# @return [Guard::Listener] the chosen listener
|
29
|
-
#
|
30
|
-
def self.select_and_init(*args)
|
31
|
-
if mac? && Darwin.usable?
|
32
|
-
Darwin.new(*args)
|
33
|
-
elsif linux? && Linux.usable?
|
34
|
-
Linux.new(*args)
|
35
|
-
elsif windows? && Windows.usable?
|
36
|
-
Windows.new(*args)
|
37
|
-
else
|
38
|
-
UI.info 'Using polling (Please help us to support your system better than that).'
|
39
|
-
Polling.new(*args)
|
40
|
-
end
|
41
|
-
end
|
42
|
-
|
43
|
-
# Initialize the listener.
|
44
|
-
#
|
45
|
-
# @param [String] directory the root directory to listen to
|
46
|
-
# @option options [Boolean] relativize_paths use only relative paths
|
47
|
-
# @option options [Array<String>] ignore_paths the paths to ignore by the listener
|
48
|
-
#
|
49
|
-
def initialize(directory = Dir.pwd, options = {})
|
50
|
-
@directory = directory.to_s
|
51
|
-
@sha1_checksums_hash = {}
|
52
|
-
@file_timestamp_hash = {}
|
53
|
-
@relativize_paths = options.fetch(:relativize_paths, true)
|
54
|
-
@changed_files = []
|
55
|
-
@locked = false
|
56
|
-
@ignore_paths = DEFAULT_IGNORE_PATHS
|
57
|
-
@ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
|
58
|
-
@watch_all_modifications = options.fetch(:watch_all_modifications, false)
|
59
|
-
|
60
|
-
update_last_event
|
61
|
-
start_reactor
|
62
|
-
end
|
63
|
-
|
64
|
-
# Start the listener thread.
|
65
|
-
#
|
66
|
-
def start_reactor
|
67
|
-
return if ENV["GUARD_ENV"] == 'test'
|
68
|
-
|
69
|
-
Thread.new do
|
70
|
-
loop do
|
71
|
-
if @changed_files != [] && !@locked
|
72
|
-
changed_files = @changed_files.dup
|
73
|
-
clear_changed_files
|
74
|
-
::Guard.run_on_change(changed_files)
|
75
|
-
else
|
76
|
-
sleep 0.1
|
77
|
-
end
|
78
|
-
end
|
79
|
-
end
|
80
|
-
end
|
81
|
-
|
82
|
-
# Start watching the root directory.
|
83
|
-
#
|
84
|
-
def start
|
85
|
-
watch(@directory)
|
86
|
-
timestamp_files
|
87
|
-
end
|
88
|
-
|
89
|
-
# Stop listening for events.
|
90
|
-
#
|
91
|
-
def stop
|
92
|
-
end
|
93
|
-
|
94
|
-
# Lock the listener to ignore change events.
|
95
|
-
#
|
96
|
-
def lock
|
97
|
-
@locked = true
|
98
|
-
end
|
99
|
-
|
100
|
-
# Unlock the listener to listen again to change events.
|
101
|
-
#
|
102
|
-
def unlock
|
103
|
-
@locked = false
|
104
|
-
end
|
105
|
-
|
106
|
-
# Clear the list of changed files.
|
107
|
-
#
|
108
|
-
def clear_changed_files
|
109
|
-
@changed_files.clear
|
110
|
-
end
|
111
|
-
|
112
|
-
# Store a listener callback.
|
113
|
-
#
|
114
|
-
# @param [Block] callback the callback to store
|
115
|
-
#
|
116
|
-
def on_change(&callback)
|
117
|
-
@callback = callback
|
118
|
-
end
|
119
|
-
|
120
|
-
# Updates the timestamp of the last event.
|
121
|
-
#
|
122
|
-
def update_last_event
|
123
|
-
@last_event = Time.now
|
124
|
-
end
|
125
|
-
|
126
|
-
# Get the modified files.
|
127
|
-
#
|
128
|
-
# If the `:watch_all_modifications` option is true, then moved and
|
129
|
-
# deleted files are also reported, but prefixed by an exclamation point.
|
130
|
-
#
|
131
|
-
# @example Deleted or moved file
|
132
|
-
# !/home/user/dir/file.rb
|
133
|
-
#
|
134
|
-
# @param [Array<String>] dirs the watched directories
|
135
|
-
# @param [Hash] options the listener options
|
136
|
-
# @option options [Symbol] all whether to files in sub directories
|
137
|
-
# @return [Array<String>] paths of files that have been modified
|
138
|
-
#
|
139
|
-
def modified_files(dirs, options = {})
|
140
|
-
last_event = @last_event
|
141
|
-
files = []
|
142
|
-
if @watch_all_modifications
|
143
|
-
deleted_files = @file_timestamp_hash.collect do |path, ts|
|
144
|
-
unless File.exists?(path)
|
145
|
-
@sha1_checksums_hash.delete(path)
|
146
|
-
@file_timestamp_hash.delete(path)
|
147
|
-
"!#{path}"
|
148
|
-
end
|
149
|
-
end
|
150
|
-
files.concat(deleted_files.compact)
|
151
|
-
end
|
152
|
-
update_last_event
|
153
|
-
files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
|
154
|
-
|
155
|
-
relativize_paths(files)
|
156
|
-
end
|
157
|
-
|
158
|
-
# Register a directory to watch.
|
159
|
-
# Must be implemented by the subclasses.
|
160
|
-
#
|
161
|
-
# @param [String] directory the directory to watch
|
162
|
-
#
|
163
|
-
def watch(directory)
|
164
|
-
raise NotImplementedError, "do whatever you want here, given the directory as only argument"
|
165
|
-
end
|
166
|
-
|
167
|
-
# Get all files that are in the watched directory.
|
168
|
-
#
|
169
|
-
# @return [Array<String>] the list of files
|
170
|
-
#
|
171
|
-
def all_files
|
172
|
-
potentially_modified_files([@directory], :all => true)
|
173
|
-
end
|
174
|
-
|
175
|
-
# Scopes all given paths to the current directory.
|
176
|
-
#
|
177
|
-
# @param [Array<String>] paths the paths to change
|
178
|
-
# @return [Array<String>] all paths now relative to the current dir
|
179
|
-
#
|
180
|
-
def relativize_paths(paths)
|
181
|
-
return paths unless relativize_paths?
|
182
|
-
paths.map do |path|
|
183
|
-
path.gsub(%r{^(!)?#{ @directory }/},'\1')
|
184
|
-
end
|
185
|
-
end
|
186
|
-
|
187
|
-
# Use paths relative to the current directory.
|
188
|
-
#
|
189
|
-
# @return [Boolean] whether to use relative or absolute paths
|
190
|
-
#
|
191
|
-
def relativize_paths?
|
192
|
-
!!@relativize_paths
|
193
|
-
end
|
194
|
-
|
195
|
-
# Populate initial timestamp file hash to watch for deleted or moved files.
|
196
|
-
#
|
197
|
-
def timestamp_files
|
198
|
-
all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
|
199
|
-
end
|
200
|
-
|
201
|
-
# Removes the ignored paths from the directory list.
|
202
|
-
#
|
203
|
-
# @param [Array<String>] dirs the directory to listen to
|
204
|
-
# @param [Array<String>] ignore_paths the paths to ignore
|
205
|
-
# @return children of the passed dirs that are not in the ignore_paths list
|
206
|
-
#
|
207
|
-
def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
|
208
|
-
Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
|
209
|
-
ignore_paths.include?(File.basename(path))
|
210
|
-
end
|
211
|
-
end
|
212
|
-
|
213
|
-
private
|
214
|
-
|
215
|
-
# Gets a list of files that are in the modified directories.
|
216
|
-
#
|
217
|
-
# @param [Array<String>] dirs the list of directories
|
218
|
-
# @param [Hash] options the find file option
|
219
|
-
# @option options [Symbol] all whether to files in sub directories
|
220
|
-
#
|
221
|
-
def potentially_modified_files(dirs, options = {})
|
222
|
-
paths = exclude_ignored_paths(dirs)
|
223
|
-
|
224
|
-
if options[:all]
|
225
|
-
paths.inject([]) do |array, path|
|
226
|
-
if File.file?(path)
|
227
|
-
array << path
|
228
|
-
else
|
229
|
-
array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
|
230
|
-
end
|
231
|
-
array
|
232
|
-
end
|
233
|
-
else
|
234
|
-
paths.select { |path| File.file?(path) }
|
235
|
-
end
|
236
|
-
end
|
237
|
-
|
238
|
-
# Test if the file content has changed.
|
239
|
-
#
|
240
|
-
# Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
|
241
|
-
# both values down to the second for the comparison.
|
242
|
-
#
|
243
|
-
# ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
|
244
|
-
#
|
245
|
-
# @param [String] path the file path
|
246
|
-
# @param [Time] last_event the time of the last event
|
247
|
-
# @return [Boolean] Whether the file content has changed or not.
|
248
|
-
#
|
249
|
-
def file_modified?(path, last_event)
|
250
|
-
ctime = File.ctime(path).to_i
|
251
|
-
mtime = File.mtime(path).to_i
|
252
|
-
if [mtime, ctime].max == last_event.to_i
|
253
|
-
file_content_modified?(path, sha1_checksum(path))
|
254
|
-
elsif mtime > last_event.to_i
|
255
|
-
set_sha1_checksums_hash(path, sha1_checksum(path))
|
256
|
-
true
|
257
|
-
elsif @watch_all_modifications
|
258
|
-
ts = file_timestamp(path)
|
259
|
-
if ts != @file_timestamp_hash[path]
|
260
|
-
set_file_timestamp_hash(path, ts)
|
261
|
-
true
|
262
|
-
end
|
263
|
-
else
|
264
|
-
false
|
265
|
-
end
|
266
|
-
rescue
|
267
|
-
false
|
268
|
-
end
|
269
|
-
|
270
|
-
# Tests if the file content has been modified by
|
271
|
-
# comparing the SHA1 checksum.
|
272
|
-
#
|
273
|
-
# @param [String] path the file path
|
274
|
-
# @param [String] sha1_checksum the checksum of the file
|
275
|
-
#
|
276
|
-
def file_content_modified?(path, sha1_checksum)
|
277
|
-
if @sha1_checksums_hash[path] != sha1_checksum
|
278
|
-
set_sha1_checksums_hash(path, sha1_checksum)
|
279
|
-
true
|
280
|
-
else
|
281
|
-
false
|
282
|
-
end
|
283
|
-
end
|
284
|
-
|
285
|
-
# Set save a files current timestamp
|
286
|
-
#
|
287
|
-
# @param [String] path the file path
|
288
|
-
# @param [Int] file_timestamp the files modified timestamp
|
289
|
-
#
|
290
|
-
def set_file_timestamp_hash(path, file_timestamp)
|
291
|
-
@file_timestamp_hash[path] = file_timestamp
|
292
|
-
end
|
293
|
-
|
294
|
-
# Set the current checksum of a file.
|
295
|
-
#
|
296
|
-
# @param [String] path the file path
|
297
|
-
# @param [String] sha1_checksum the checksum of the file
|
298
|
-
#
|
299
|
-
def set_sha1_checksums_hash(path, sha1_checksum)
|
300
|
-
@sha1_checksums_hash[path] = sha1_checksum
|
301
|
-
end
|
302
|
-
|
303
|
-
# Gets a files modified timestamp
|
304
|
-
#
|
305
|
-
# @path [String] path the file path
|
306
|
-
# @return [Int] file modified timestamp
|
307
|
-
#
|
308
|
-
def file_timestamp(path)
|
309
|
-
File.mtime(path).to_i
|
310
|
-
end
|
311
|
-
|
312
|
-
# Calculates the SHA1 checksum of a file.
|
313
|
-
#
|
314
|
-
# @param [String] path the path to the file
|
315
|
-
# @return [String] the SHA1 checksum
|
316
|
-
#
|
317
|
-
def sha1_checksum(path)
|
318
|
-
Digest::SHA1.file(path).to_s
|
319
|
-
end
|
320
|
-
|
321
|
-
# Test if the OS is Mac OS X.
|
322
|
-
#
|
323
|
-
# @return [Boolean] Whether the OS is Mac OS X
|
324
|
-
#
|
325
|
-
def self.mac?
|
326
|
-
RbConfig::CONFIG['target_os'] =~ /darwin/i
|
327
|
-
end
|
328
|
-
|
329
|
-
# Test if the OS is Linux.
|
330
|
-
#
|
331
|
-
# @return [Boolean] Whether the OS is Linux
|
332
|
-
#
|
333
|
-
def self.linux?
|
334
|
-
RbConfig::CONFIG['target_os'] =~ /linux/i
|
335
|
-
end
|
336
|
-
|
337
|
-
# Test if the OS is Windows.
|
338
|
-
#
|
339
|
-
# @return [Boolean] Whether the OS is Windows
|
340
|
-
#
|
341
|
-
def self.windows?
|
342
|
-
RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
|
343
|
-
end
|
344
|
-
|
345
|
-
end
|
346
|
-
end
|
1
|
+
require 'rbconfig'
|
2
|
+
require 'digest/sha1'
|
3
|
+
|
4
|
+
module Guard
|
5
|
+
|
6
|
+
autoload :Darwin, 'guard/listeners/darwin'
|
7
|
+
autoload :Linux, 'guard/listeners/linux'
|
8
|
+
autoload :Windows, 'guard/listeners/windows'
|
9
|
+
autoload :Polling, 'guard/listeners/polling'
|
10
|
+
|
11
|
+
# The Listener is the base class for all listener
|
12
|
+
# implementations.
|
13
|
+
#
|
14
|
+
# @abstract
|
15
|
+
#
|
16
|
+
class Listener
|
17
|
+
|
18
|
+
# Default paths that gets ignored by the listener
|
19
|
+
DEFAULT_IGNORE_PATHS = %w[. .. .bundle .git log tmp vendor]
|
20
|
+
|
21
|
+
attr_accessor :changed_files
|
22
|
+
attr_reader :directory, :ignore_paths, :locked
|
23
|
+
|
24
|
+
# Select the appropriate listener implementation for the
|
25
|
+
# current OS and initializes it.
|
26
|
+
#
|
27
|
+
# @param [Array] args the arguments for the listener
|
28
|
+
# @return [Guard::Listener] the chosen listener
|
29
|
+
#
|
30
|
+
def self.select_and_init(*args)
|
31
|
+
if mac? && Darwin.usable?
|
32
|
+
Darwin.new(*args)
|
33
|
+
elsif linux? && Linux.usable?
|
34
|
+
Linux.new(*args)
|
35
|
+
elsif windows? && Windows.usable?
|
36
|
+
Windows.new(*args)
|
37
|
+
else
|
38
|
+
UI.info 'Using polling (Please help us to support your system better than that).'
|
39
|
+
Polling.new(*args)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
# Initialize the listener.
|
44
|
+
#
|
45
|
+
# @param [String] directory the root directory to listen to
|
46
|
+
# @option options [Boolean] relativize_paths use only relative paths
|
47
|
+
# @option options [Array<String>] ignore_paths the paths to ignore by the listener
|
48
|
+
#
|
49
|
+
def initialize(directory = Dir.pwd, options = {})
|
50
|
+
@directory = directory.to_s
|
51
|
+
@sha1_checksums_hash = {}
|
52
|
+
@file_timestamp_hash = {}
|
53
|
+
@relativize_paths = options.fetch(:relativize_paths, true)
|
54
|
+
@changed_files = []
|
55
|
+
@locked = false
|
56
|
+
@ignore_paths = DEFAULT_IGNORE_PATHS
|
57
|
+
@ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
|
58
|
+
@watch_all_modifications = options.fetch(:watch_all_modifications, false)
|
59
|
+
|
60
|
+
update_last_event
|
61
|
+
start_reactor
|
62
|
+
end
|
63
|
+
|
64
|
+
# Start the listener thread.
|
65
|
+
#
|
66
|
+
def start_reactor
|
67
|
+
return if ENV["GUARD_ENV"] == 'test'
|
68
|
+
|
69
|
+
Thread.new do
|
70
|
+
loop do
|
71
|
+
if @changed_files != [] && !@locked
|
72
|
+
changed_files = @changed_files.dup
|
73
|
+
clear_changed_files
|
74
|
+
::Guard.run_on_change(changed_files)
|
75
|
+
else
|
76
|
+
sleep 0.1
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
# Start watching the root directory.
|
83
|
+
#
|
84
|
+
def start
|
85
|
+
watch(@directory)
|
86
|
+
timestamp_files
|
87
|
+
end
|
88
|
+
|
89
|
+
# Stop listening for events.
|
90
|
+
#
|
91
|
+
def stop
|
92
|
+
end
|
93
|
+
|
94
|
+
# Lock the listener to ignore change events.
|
95
|
+
#
|
96
|
+
def lock
|
97
|
+
@locked = true
|
98
|
+
end
|
99
|
+
|
100
|
+
# Unlock the listener to listen again to change events.
|
101
|
+
#
|
102
|
+
def unlock
|
103
|
+
@locked = false
|
104
|
+
end
|
105
|
+
|
106
|
+
# Clear the list of changed files.
|
107
|
+
#
|
108
|
+
def clear_changed_files
|
109
|
+
@changed_files.clear
|
110
|
+
end
|
111
|
+
|
112
|
+
# Store a listener callback.
|
113
|
+
#
|
114
|
+
# @param [Block] callback the callback to store
|
115
|
+
#
|
116
|
+
def on_change(&callback)
|
117
|
+
@callback = callback
|
118
|
+
end
|
119
|
+
|
120
|
+
# Updates the timestamp of the last event.
|
121
|
+
#
|
122
|
+
def update_last_event
|
123
|
+
@last_event = Time.now
|
124
|
+
end
|
125
|
+
|
126
|
+
# Get the modified files.
|
127
|
+
#
|
128
|
+
# If the `:watch_all_modifications` option is true, then moved and
|
129
|
+
# deleted files are also reported, but prefixed by an exclamation point.
|
130
|
+
#
|
131
|
+
# @example Deleted or moved file
|
132
|
+
# !/home/user/dir/file.rb
|
133
|
+
#
|
134
|
+
# @param [Array<String>] dirs the watched directories
|
135
|
+
# @param [Hash] options the listener options
|
136
|
+
# @option options [Symbol] all whether to files in sub directories
|
137
|
+
# @return [Array<String>] paths of files that have been modified
|
138
|
+
#
|
139
|
+
def modified_files(dirs, options = {})
|
140
|
+
last_event = @last_event
|
141
|
+
files = []
|
142
|
+
if @watch_all_modifications
|
143
|
+
deleted_files = @file_timestamp_hash.collect do |path, ts|
|
144
|
+
unless File.exists?(path)
|
145
|
+
@sha1_checksums_hash.delete(path)
|
146
|
+
@file_timestamp_hash.delete(path)
|
147
|
+
"!#{path}"
|
148
|
+
end
|
149
|
+
end
|
150
|
+
files.concat(deleted_files.compact)
|
151
|
+
end
|
152
|
+
update_last_event
|
153
|
+
files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
|
154
|
+
|
155
|
+
relativize_paths(files)
|
156
|
+
end
|
157
|
+
|
158
|
+
# Register a directory to watch.
|
159
|
+
# Must be implemented by the subclasses.
|
160
|
+
#
|
161
|
+
# @param [String] directory the directory to watch
|
162
|
+
#
|
163
|
+
def watch(directory)
|
164
|
+
raise NotImplementedError, "do whatever you want here, given the directory as only argument"
|
165
|
+
end
|
166
|
+
|
167
|
+
# Get all files that are in the watched directory.
|
168
|
+
#
|
169
|
+
# @return [Array<String>] the list of files
|
170
|
+
#
|
171
|
+
def all_files
|
172
|
+
potentially_modified_files([@directory], :all => true)
|
173
|
+
end
|
174
|
+
|
175
|
+
# Scopes all given paths to the current directory.
|
176
|
+
#
|
177
|
+
# @param [Array<String>] paths the paths to change
|
178
|
+
# @return [Array<String>] all paths now relative to the current dir
|
179
|
+
#
|
180
|
+
def relativize_paths(paths)
|
181
|
+
return paths unless relativize_paths?
|
182
|
+
paths.map do |path|
|
183
|
+
path.gsub(%r{^(!)?#{ @directory }/},'\1')
|
184
|
+
end
|
185
|
+
end
|
186
|
+
|
187
|
+
# Use paths relative to the current directory.
|
188
|
+
#
|
189
|
+
# @return [Boolean] whether to use relative or absolute paths
|
190
|
+
#
|
191
|
+
def relativize_paths?
|
192
|
+
!!@relativize_paths
|
193
|
+
end
|
194
|
+
|
195
|
+
# Populate initial timestamp file hash to watch for deleted or moved files.
|
196
|
+
#
|
197
|
+
def timestamp_files
|
198
|
+
all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
|
199
|
+
end
|
200
|
+
|
201
|
+
# Removes the ignored paths from the directory list.
|
202
|
+
#
|
203
|
+
# @param [Array<String>] dirs the directory to listen to
|
204
|
+
# @param [Array<String>] ignore_paths the paths to ignore
|
205
|
+
# @return children of the passed dirs that are not in the ignore_paths list
|
206
|
+
#
|
207
|
+
def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
|
208
|
+
Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
|
209
|
+
ignore_paths.include?(File.basename(path))
|
210
|
+
end
|
211
|
+
end
|
212
|
+
|
213
|
+
private
|
214
|
+
|
215
|
+
# Gets a list of files that are in the modified directories.
|
216
|
+
#
|
217
|
+
# @param [Array<String>] dirs the list of directories
|
218
|
+
# @param [Hash] options the find file option
|
219
|
+
# @option options [Symbol] all whether to files in sub directories
|
220
|
+
#
|
221
|
+
def potentially_modified_files(dirs, options = {})
|
222
|
+
paths = exclude_ignored_paths(dirs)
|
223
|
+
|
224
|
+
if options[:all]
|
225
|
+
paths.inject([]) do |array, path|
|
226
|
+
if File.file?(path)
|
227
|
+
array << path
|
228
|
+
else
|
229
|
+
array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
|
230
|
+
end
|
231
|
+
array
|
232
|
+
end
|
233
|
+
else
|
234
|
+
paths.select { |path| File.file?(path) }
|
235
|
+
end
|
236
|
+
end
|
237
|
+
|
238
|
+
# Test if the file content has changed.
|
239
|
+
#
|
240
|
+
# Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
|
241
|
+
# both values down to the second for the comparison.
|
242
|
+
#
|
243
|
+
# ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
|
244
|
+
#
|
245
|
+
# @param [String] path the file path
|
246
|
+
# @param [Time] last_event the time of the last event
|
247
|
+
# @return [Boolean] Whether the file content has changed or not.
|
248
|
+
#
|
249
|
+
def file_modified?(path, last_event)
|
250
|
+
ctime = File.ctime(path).to_i
|
251
|
+
mtime = File.mtime(path).to_i
|
252
|
+
if [mtime, ctime].max == last_event.to_i
|
253
|
+
file_content_modified?(path, sha1_checksum(path))
|
254
|
+
elsif mtime > last_event.to_i
|
255
|
+
set_sha1_checksums_hash(path, sha1_checksum(path))
|
256
|
+
true
|
257
|
+
elsif @watch_all_modifications
|
258
|
+
ts = file_timestamp(path)
|
259
|
+
if ts != @file_timestamp_hash[path]
|
260
|
+
set_file_timestamp_hash(path, ts)
|
261
|
+
true
|
262
|
+
end
|
263
|
+
else
|
264
|
+
false
|
265
|
+
end
|
266
|
+
rescue
|
267
|
+
false
|
268
|
+
end
|
269
|
+
|
270
|
+
# Tests if the file content has been modified by
|
271
|
+
# comparing the SHA1 checksum.
|
272
|
+
#
|
273
|
+
# @param [String] path the file path
|
274
|
+
# @param [String] sha1_checksum the checksum of the file
|
275
|
+
#
|
276
|
+
def file_content_modified?(path, sha1_checksum)
|
277
|
+
if @sha1_checksums_hash[path] != sha1_checksum
|
278
|
+
set_sha1_checksums_hash(path, sha1_checksum)
|
279
|
+
true
|
280
|
+
else
|
281
|
+
false
|
282
|
+
end
|
283
|
+
end
|
284
|
+
|
285
|
+
# Set save a files current timestamp
|
286
|
+
#
|
287
|
+
# @param [String] path the file path
|
288
|
+
# @param [Int] file_timestamp the files modified timestamp
|
289
|
+
#
|
290
|
+
def set_file_timestamp_hash(path, file_timestamp)
|
291
|
+
@file_timestamp_hash[path] = file_timestamp
|
292
|
+
end
|
293
|
+
|
294
|
+
# Set the current checksum of a file.
|
295
|
+
#
|
296
|
+
# @param [String] path the file path
|
297
|
+
# @param [String] sha1_checksum the checksum of the file
|
298
|
+
#
|
299
|
+
def set_sha1_checksums_hash(path, sha1_checksum)
|
300
|
+
@sha1_checksums_hash[path] = sha1_checksum
|
301
|
+
end
|
302
|
+
|
303
|
+
# Gets a files modified timestamp
|
304
|
+
#
|
305
|
+
# @path [String] path the file path
|
306
|
+
# @return [Int] file modified timestamp
|
307
|
+
#
|
308
|
+
def file_timestamp(path)
|
309
|
+
File.mtime(path).to_i
|
310
|
+
end
|
311
|
+
|
312
|
+
# Calculates the SHA1 checksum of a file.
|
313
|
+
#
|
314
|
+
# @param [String] path the path to the file
|
315
|
+
# @return [String] the SHA1 checksum
|
316
|
+
#
|
317
|
+
def sha1_checksum(path)
|
318
|
+
Digest::SHA1.file(path).to_s
|
319
|
+
end
|
320
|
+
|
321
|
+
# Test if the OS is Mac OS X.
|
322
|
+
#
|
323
|
+
# @return [Boolean] Whether the OS is Mac OS X
|
324
|
+
#
|
325
|
+
def self.mac?
|
326
|
+
RbConfig::CONFIG['target_os'] =~ /darwin/i
|
327
|
+
end
|
328
|
+
|
329
|
+
# Test if the OS is Linux.
|
330
|
+
#
|
331
|
+
# @return [Boolean] Whether the OS is Linux
|
332
|
+
#
|
333
|
+
def self.linux?
|
334
|
+
RbConfig::CONFIG['target_os'] =~ /linux/i
|
335
|
+
end
|
336
|
+
|
337
|
+
# Test if the OS is Windows.
|
338
|
+
#
|
339
|
+
# @return [Boolean] Whether the OS is Windows
|
340
|
+
#
|
341
|
+
def self.windows?
|
342
|
+
RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
|
343
|
+
end
|
344
|
+
|
345
|
+
end
|
346
|
+
end
|