guard 0.8.4 → 0.8.5

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/lib/guard/hook.rb CHANGED
@@ -1,118 +1,118 @@
1
- module Guard
2
-
3
- # Guard has a hook mechanism that allows you to insert callbacks for individual Guards.
4
- # By default, each of the Guard instance methods has a "_begin" and an "_end" hook.
5
- # For example, the Guard::Guard#start method has a :start_begin hook that is runs immediately
6
- # before Guard::Guard#start, and a :start_end hook that runs immediately after Guard::Guard#start.
7
- #
8
- # Read more about [hooks and callbacks on the wiki](https://github.com/guard/guard/wiki/Hooks-and-callbacks).
9
- #
10
- module Hook
11
-
12
- # The Hook module gets included.
13
- #
14
- # @param [Class] base the class that includes the module
15
- #
16
- def self.included(base)
17
- base.send :include, InstanceMethods
18
- end
19
-
20
- # Instance methods that gets included in the base class.
21
- #
22
- module InstanceMethods
23
-
24
- # When event is a Symbol, {#hook} will generate a hook name
25
- # by concatenating the method name from where {#hook} is called
26
- # with the given Symbol.
27
- #
28
- # @example Add a hook with a Symbol
29
- #
30
- # def run_all
31
- # hook :foo
32
- # end
33
- #
34
- # Here, when {Guard::Guard#run_all} is called, {#hook} will notify callbacks
35
- # registered for the "run_all_foo" event.
36
- #
37
- # When event is a String, {#hook} will directly turn the String
38
- # into a Symbol.
39
- #
40
- # @example Add a hook with a String
41
- #
42
- # def run_all
43
- # hook "foo_bar"
44
- # end
45
- #
46
- # When {Guard::Guard#run_all} is called, {#hook} will notify callbacks
47
- # registered for the "foo_bar" event.
48
- #
49
- # @param [Symbol, String] event the name of the Guard event
50
- # @param [Array] args the parameters are passed as is to the callbacks registered for the given event.
51
- #
52
- def hook(event, *args)
53
- hook_name = if event.is_a? Symbol
54
- calling_method = caller[0][/`([^']*)'/, 1]
55
- "#{ calling_method }_#{ event }"
56
- else
57
- event
58
- end.to_sym
59
-
60
- UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
61
-
62
- Hook.notify(self.class, hook_name, *args)
63
- end
64
- end
65
-
66
- class << self
67
-
68
- # Get all callbacks.
69
- #
70
- def callbacks
71
- @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
72
- end
73
-
74
- # Add a callback.
75
- #
76
- # @param [Block] listener the listener to notify
77
- # @param [Guard::Guard] guard_class the Guard class to add the callback
78
- # @param [Array<Symbol>] events the events to register
79
- #
80
- def add_callback(listener, guard_class, events)
81
- _events = events.is_a?(Array) ? events : [events]
82
- _events.each do |event|
83
- callbacks[[guard_class, event]] << listener
84
- end
85
- end
86
-
87
- # Checks if a callback has been registered.
88
- #
89
- # @param [Block] listener the listener to notify
90
- # @param [Guard::Guard] guard_class the Guard class to add the callback
91
- # @param [Symbol] event the event to look for
92
- #
93
- def has_callback?(listener, guard_class, event)
94
- callbacks[[guard_class, event]].include?(listener)
95
- end
96
-
97
- # Notify a callback.
98
- #
99
- # @param [Guard::Guard] guard_class the Guard class to add the callback
100
- # @param [Symbol] event the event to trigger
101
- # @param [Array] args the arguments for the listener
102
- #
103
- def notify(guard_class, event, *args)
104
- callbacks[[guard_class, event]].each do |listener|
105
- listener.call(guard_class, event, *args)
106
- end
107
- end
108
-
109
- # Reset all callbacks.
110
- #
111
- def reset_callbacks!
112
- @callbacks = nil
113
- end
114
-
115
- end
116
-
117
- end
118
- end
1
+ module Guard
2
+
3
+ # Guard has a hook mechanism that allows you to insert callbacks for individual Guards.
4
+ # By default, each of the Guard instance methods has a "_begin" and an "_end" hook.
5
+ # For example, the Guard::Guard#start method has a :start_begin hook that is runs immediately
6
+ # before Guard::Guard#start, and a :start_end hook that runs immediately after Guard::Guard#start.
7
+ #
8
+ # Read more about [hooks and callbacks on the wiki](https://github.com/guard/guard/wiki/Hooks-and-callbacks).
9
+ #
10
+ module Hook
11
+
12
+ # The Hook module gets included.
13
+ #
14
+ # @param [Class] base the class that includes the module
15
+ #
16
+ def self.included(base)
17
+ base.send :include, InstanceMethods
18
+ end
19
+
20
+ # Instance methods that gets included in the base class.
21
+ #
22
+ module InstanceMethods
23
+
24
+ # When event is a Symbol, {#hook} will generate a hook name
25
+ # by concatenating the method name from where {#hook} is called
26
+ # with the given Symbol.
27
+ #
28
+ # @example Add a hook with a Symbol
29
+ #
30
+ # def run_all
31
+ # hook :foo
32
+ # end
33
+ #
34
+ # Here, when {Guard::Guard#run_all} is called, {#hook} will notify callbacks
35
+ # registered for the "run_all_foo" event.
36
+ #
37
+ # When event is a String, {#hook} will directly turn the String
38
+ # into a Symbol.
39
+ #
40
+ # @example Add a hook with a String
41
+ #
42
+ # def run_all
43
+ # hook "foo_bar"
44
+ # end
45
+ #
46
+ # When {Guard::Guard#run_all} is called, {#hook} will notify callbacks
47
+ # registered for the "foo_bar" event.
48
+ #
49
+ # @param [Symbol, String] event the name of the Guard event
50
+ # @param [Array] args the parameters are passed as is to the callbacks registered for the given event.
51
+ #
52
+ def hook(event, *args)
53
+ hook_name = if event.is_a? Symbol
54
+ calling_method = caller[0][/`([^']*)'/, 1]
55
+ "#{ calling_method }_#{ event }"
56
+ else
57
+ event
58
+ end.to_sym
59
+
60
+ UI.debug "Hook :#{ hook_name } executed for #{ self.class }"
61
+
62
+ Hook.notify(self.class, hook_name, *args)
63
+ end
64
+ end
65
+
66
+ class << self
67
+
68
+ # Get all callbacks.
69
+ #
70
+ def callbacks
71
+ @callbacks ||= Hash.new { |hash, key| hash[key] = [] }
72
+ end
73
+
74
+ # Add a callback.
75
+ #
76
+ # @param [Block] listener the listener to notify
77
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
78
+ # @param [Array<Symbol>] events the events to register
79
+ #
80
+ def add_callback(listener, guard_class, events)
81
+ _events = events.is_a?(Array) ? events : [events]
82
+ _events.each do |event|
83
+ callbacks[[guard_class, event]] << listener
84
+ end
85
+ end
86
+
87
+ # Checks if a callback has been registered.
88
+ #
89
+ # @param [Block] listener the listener to notify
90
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
91
+ # @param [Symbol] event the event to look for
92
+ #
93
+ def has_callback?(listener, guard_class, event)
94
+ callbacks[[guard_class, event]].include?(listener)
95
+ end
96
+
97
+ # Notify a callback.
98
+ #
99
+ # @param [Guard::Guard] guard_class the Guard class to add the callback
100
+ # @param [Symbol] event the event to trigger
101
+ # @param [Array] args the arguments for the listener
102
+ #
103
+ def notify(guard_class, event, *args)
104
+ callbacks[[guard_class, event]].each do |listener|
105
+ listener.call(guard_class, event, *args)
106
+ end
107
+ end
108
+
109
+ # Reset all callbacks.
110
+ #
111
+ def reset_callbacks!
112
+ @callbacks = nil
113
+ end
114
+
115
+ end
116
+
117
+ end
118
+ end
@@ -1,44 +1,110 @@
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
- # Start the interactor in its own thread.
15
- #
16
- def start
17
- return if ENV["GUARD_ENV"] == 'test'
18
-
19
- if !@thread || @thread.stop?
20
- @thread = Thread.new do
21
- while entry = $stdin.gets.chomp
22
- case entry
23
- when 'stop', 'quit', 'exit', 's', 'q', 'e'
24
- ::Guard.stop
25
- when 'reload', 'r', 'z'
26
- ::Guard::Dsl.reevaluate_guardfile
27
- ::Guard.reload
28
- when 'pause', 'p'
29
- ::Guard.pause
30
- else
31
- ::Guard.run_all
32
- end
33
- end
34
- end
35
- end
36
- end
37
-
38
- def stop_if_not_current
39
- unless Thread.current == @thread
40
- @thread.kill
41
- end
42
- end
43
- end
44
- 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
+ # It's also possible to scope `reload` and `run all` actions to only a specified group or a guard.
14
+ #
15
+ # @example `backend reload` will only reload backend group
16
+ # @example `spork reload` will only reload rspec guard
17
+ # @example `jasmine` will only run all jasmine specs
18
+ #
19
+ class Interactor
20
+
21
+ STOP_ACTIONS = %w[stop quit exit s q e]
22
+ RELOAD_ACTIONS = %w[reload r z]
23
+ PAUSE_ACTIONS = %w[pause p]
24
+
25
+ # Start the interactor in its own thread.
26
+ #
27
+ def start
28
+ return if ENV["GUARD_ENV"] == 'test'
29
+
30
+ if !@thread || @thread.stop?
31
+ @thread = Thread.new do
32
+ while entry = $stdin.gets.chomp
33
+ scopes, action = extract_scopes_and_action(entry)
34
+ case action
35
+ when :stop
36
+ ::Guard.stop
37
+ when :pause
38
+ ::Guard.pause
39
+ when :reload
40
+ ::Guard::Dsl.reevaluate_guardfile if scopes.empty?
41
+ ::Guard.reload(scopes)
42
+ when :run_all
43
+ ::Guard.run_all(scopes)
44
+ end
45
+ end
46
+ end
47
+ end
48
+ end
49
+
50
+ # Extract guard or group scope and action from Interactor entry
51
+ #
52
+ # @example `spork reload` will only reload rspec
53
+ # @example `jasmine` will only run all jasmine specs
54
+ #
55
+ # @param [String] Interactor entry gets from $stdin
56
+ # @return [Array] entry group or guard scope hash and action
57
+ def extract_scopes_and_action(entry)
58
+ scopes = {}
59
+ entries = entry.split(' ')
60
+ case entries.length
61
+ when 1
62
+ unless action = action_from_entry(entries[0])
63
+ scopes = scopes_from_entry(entries[0])
64
+ end
65
+ when 2
66
+ scopes = scopes_from_entry(entries[0])
67
+ action = action_from_entry(entries[1])
68
+ end
69
+ action ||= :run_all
70
+ [scopes, action]
71
+ end
72
+
73
+ # Extract guard or group scope from entry if valid
74
+ #
75
+ # @param [String] Interactor entry gets from $stdin
76
+ # @return [Hash] An hash with a guard or a group scope
77
+ def scopes_from_entry(entry)
78
+ scopes = {}
79
+ if guard = ::Guard.guards(entry)
80
+ scopes[:guard] = guard
81
+ end
82
+ if group = ::Guard.groups(entry)
83
+ scopes[:group] = group
84
+ end
85
+ scopes
86
+ end
87
+
88
+ # Extract action from entry if an existing action is present
89
+ #
90
+ # @param [String] Interactor entry gets from $stdin
91
+ # @return [Symbol] A guard action
92
+ def action_from_entry(entry)
93
+ if STOP_ACTIONS.include?(entry)
94
+ :stop
95
+ elsif RELOAD_ACTIONS.include?(entry)
96
+ :reload
97
+ elsif PAUSE_ACTIONS.include?(entry)
98
+ :pause
99
+ end
100
+ end
101
+
102
+ # Kill interactor thread if not current
103
+ #
104
+ def stop_if_not_current
105
+ unless Thread.current == @thread
106
+ @thread.kill
107
+ end
108
+ end
109
+ end
110
+ end
@@ -1,350 +1,350 @@
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
23
-
24
- def paused?
25
- @paused
26
- end
27
-
28
- # Select the appropriate listener implementation for the
29
- # current OS and initializes it.
30
- #
31
- # @param [Array] args the arguments for the listener
32
- # @return [Guard::Listener] the chosen listener
33
- #
34
- def self.select_and_init(*args)
35
- if mac? && Darwin.usable?
36
- Darwin.new(*args)
37
- elsif linux? && Linux.usable?
38
- Linux.new(*args)
39
- elsif windows? && Windows.usable?
40
- Windows.new(*args)
41
- else
42
- UI.info 'Using polling (Please help us to support your system better than that).'
43
- Polling.new(*args)
44
- end
45
- end
46
-
47
- # Initialize the listener.
48
- #
49
- # @param [String] directory the root directory to listen to
50
- # @option options [Boolean] relativize_paths use only relative paths
51
- # @option options [Array<String>] ignore_paths the paths to ignore by the listener
52
- #
53
- def initialize(directory = Dir.pwd, options = {})
54
- @directory = directory.to_s
55
- @sha1_checksums_hash = {}
56
- @file_timestamp_hash = {}
57
- @relativize_paths = options.fetch(:relativize_paths, true)
58
- @changed_files = []
59
- @paused = false
60
- @ignore_paths = DEFAULT_IGNORE_PATHS
61
- @ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
62
- @watch_all_modifications = options.fetch(:watch_all_modifications, false)
63
-
64
- update_last_event
65
- start_reactor
66
- end
67
-
68
- # Start the listener thread.
69
- #
70
- def start_reactor
71
- return if ENV["GUARD_ENV"] == 'test'
72
-
73
- Thread.new do
74
- loop do
75
- if @changed_files != [] && !@paused
76
- changed_files = @changed_files.dup
77
- clear_changed_files
78
- ::Guard.run_on_change(changed_files)
79
- else
80
- sleep 0.1
81
- end
82
- end
83
- end
84
- end
85
-
86
- # Start watching the root directory.
87
- #
88
- def start
89
- watch(@directory)
90
- timestamp_files
91
- end
92
-
93
- # Stop listening for events.
94
- #
95
- def stop
96
- end
97
-
98
- # Pause the listener to ignore change events.
99
- #
100
- def pause
101
- @paused = true
102
- end
103
-
104
- # Unpause the listener to listen again to change events.
105
- #
106
- def run
107
- @paused = false
108
- end
109
-
110
- # Clear the list of changed files.
111
- #
112
- def clear_changed_files
113
- @changed_files.clear
114
- end
115
-
116
- # Store a listener callback.
117
- #
118
- # @param [Block] callback the callback to store
119
- #
120
- def on_change(&callback)
121
- @callback = callback
122
- end
123
-
124
- # Updates the timestamp of the last event.
125
- #
126
- def update_last_event
127
- @last_event = Time.now
128
- end
129
-
130
- # Get the modified files.
131
- #
132
- # If the `:watch_all_modifications` option is true, then moved and
133
- # deleted files are also reported, but prefixed by an exclamation point.
134
- #
135
- # @example Deleted or moved file
136
- # !/home/user/dir/file.rb
137
- #
138
- # @param [Array<String>] dirs the watched directories
139
- # @param [Hash] options the listener options
140
- # @option options [Symbol] all whether to files in sub directories
141
- # @return [Array<String>] paths of files that have been modified
142
- #
143
- def modified_files(dirs, options = {})
144
- last_event = @last_event
145
- files = []
146
- if @watch_all_modifications
147
- deleted_files = @file_timestamp_hash.collect do |path, ts|
148
- unless File.exists?(path)
149
- @sha1_checksums_hash.delete(path)
150
- @file_timestamp_hash.delete(path)
151
- "!#{path}"
152
- end
153
- end
154
- files.concat(deleted_files.compact)
155
- end
156
- update_last_event
157
- files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
158
-
159
- relativize_paths(files)
160
- end
161
-
162
- # Register a directory to watch.
163
- # Must be implemented by the subclasses.
164
- #
165
- # @param [String] directory the directory to watch
166
- #
167
- def watch(directory)
168
- raise NotImplementedError, "do whatever you want here, given the directory as only argument"
169
- end
170
-
171
- # Get all files that are in the watched directory.
172
- #
173
- # @return [Array<String>] the list of files
174
- #
175
- def all_files
176
- potentially_modified_files([@directory], :all => true)
177
- end
178
-
179
- # Scopes all given paths to the current directory.
180
- #
181
- # @param [Array<String>] paths the paths to change
182
- # @return [Array<String>] all paths now relative to the current dir
183
- #
184
- def relativize_paths(paths)
185
- return paths unless relativize_paths?
186
- paths.map do |path|
187
- path.gsub(%r{^(!)?#{ @directory }/},'\1')
188
- end
189
- end
190
-
191
- # Use paths relative to the current directory.
192
- #
193
- # @return [Boolean] whether to use relative or absolute paths
194
- #
195
- def relativize_paths?
196
- !!@relativize_paths
197
- end
198
-
199
- # Populate initial timestamp file hash to watch for deleted or moved files.
200
- #
201
- def timestamp_files
202
- all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
203
- end
204
-
205
- # Removes the ignored paths from the directory list.
206
- #
207
- # @param [Array<String>] dirs the directory to listen to
208
- # @param [Array<String>] ignore_paths the paths to ignore
209
- # @return children of the passed dirs that are not in the ignore_paths list
210
- #
211
- def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
212
- Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
213
- ignore_paths.include?(File.basename(path))
214
- end
215
- end
216
-
217
- private
218
-
219
- # Gets a list of files that are in the modified directories.
220
- #
221
- # @param [Array<String>] dirs the list of directories
222
- # @param [Hash] options the find file option
223
- # @option options [Symbol] all whether to files in sub directories
224
- #
225
- def potentially_modified_files(dirs, options = {})
226
- paths = exclude_ignored_paths(dirs)
227
-
228
- if options[:all]
229
- paths.inject([]) do |array, path|
230
- if File.file?(path)
231
- array << path
232
- else
233
- array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
234
- end
235
- array
236
- end
237
- else
238
- paths.select { |path| File.file?(path) }
239
- end
240
- end
241
-
242
- # Test if the file content has changed.
243
- #
244
- # Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
245
- # both values down to the second for the comparison.
246
- #
247
- # ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
248
- #
249
- # @param [String] path the file path
250
- # @param [Time] last_event the time of the last event
251
- # @return [Boolean] Whether the file content has changed or not.
252
- #
253
- def file_modified?(path, last_event)
254
- ctime = File.ctime(path).to_i
255
- mtime = File.mtime(path).to_i
256
- if [mtime, ctime].max == last_event.to_i
257
- file_content_modified?(path, sha1_checksum(path))
258
- elsif mtime > last_event.to_i
259
- set_sha1_checksums_hash(path, sha1_checksum(path))
260
- true
261
- elsif @watch_all_modifications
262
- ts = file_timestamp(path)
263
- if ts != @file_timestamp_hash[path]
264
- set_file_timestamp_hash(path, ts)
265
- true
266
- end
267
- else
268
- false
269
- end
270
- rescue
271
- false
272
- end
273
-
274
- # Tests if the file content has been modified by
275
- # comparing the SHA1 checksum.
276
- #
277
- # @param [String] path the file path
278
- # @param [String] sha1_checksum the checksum of the file
279
- #
280
- def file_content_modified?(path, sha1_checksum)
281
- if @sha1_checksums_hash[path] != sha1_checksum
282
- set_sha1_checksums_hash(path, sha1_checksum)
283
- true
284
- else
285
- false
286
- end
287
- end
288
-
289
- # Set save a files current timestamp
290
- #
291
- # @param [String] path the file path
292
- # @param [Int] file_timestamp the files modified timestamp
293
- #
294
- def set_file_timestamp_hash(path, file_timestamp)
295
- @file_timestamp_hash[path] = file_timestamp
296
- end
297
-
298
- # Set the current checksum of a file.
299
- #
300
- # @param [String] path the file path
301
- # @param [String] sha1_checksum the checksum of the file
302
- #
303
- def set_sha1_checksums_hash(path, sha1_checksum)
304
- @sha1_checksums_hash[path] = sha1_checksum
305
- end
306
-
307
- # Gets a files modified timestamp
308
- #
309
- # @path [String] path the file path
310
- # @return [Int] file modified timestamp
311
- #
312
- def file_timestamp(path)
313
- File.mtime(path).to_i
314
- end
315
-
316
- # Calculates the SHA1 checksum of a file.
317
- #
318
- # @param [String] path the path to the file
319
- # @return [String] the SHA1 checksum
320
- #
321
- def sha1_checksum(path)
322
- Digest::SHA1.file(path).to_s
323
- end
324
-
325
- # Test if the OS is Mac OS X.
326
- #
327
- # @return [Boolean] Whether the OS is Mac OS X
328
- #
329
- def self.mac?
330
- RbConfig::CONFIG['target_os'] =~ /darwin/i
331
- end
332
-
333
- # Test if the OS is Linux.
334
- #
335
- # @return [Boolean] Whether the OS is Linux
336
- #
337
- def self.linux?
338
- RbConfig::CONFIG['target_os'] =~ /linux/i
339
- end
340
-
341
- # Test if the OS is Windows.
342
- #
343
- # @return [Boolean] Whether the OS is Windows
344
- #
345
- def self.windows?
346
- RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
347
- end
348
-
349
- end
350
- 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
23
+
24
+ def paused?
25
+ @paused
26
+ end
27
+
28
+ # Select the appropriate listener implementation for the
29
+ # current OS and initializes it.
30
+ #
31
+ # @param [Array] args the arguments for the listener
32
+ # @return [Guard::Listener] the chosen listener
33
+ #
34
+ def self.select_and_init(*args)
35
+ if mac? && Darwin.usable?
36
+ Darwin.new(*args)
37
+ elsif linux? && Linux.usable?
38
+ Linux.new(*args)
39
+ elsif windows? && Windows.usable?
40
+ Windows.new(*args)
41
+ else
42
+ UI.info 'Using polling (Please help us to support your system better than that).'
43
+ Polling.new(*args)
44
+ end
45
+ end
46
+
47
+ # Initialize the listener.
48
+ #
49
+ # @param [String] directory the root directory to listen to
50
+ # @option options [Boolean] relativize_paths use only relative paths
51
+ # @option options [Array<String>] ignore_paths the paths to ignore by the listener
52
+ #
53
+ def initialize(directory = Dir.pwd, options = {})
54
+ @directory = directory.to_s
55
+ @sha1_checksums_hash = {}
56
+ @file_timestamp_hash = {}
57
+ @relativize_paths = options.fetch(:relativize_paths, true)
58
+ @changed_files = []
59
+ @paused = false
60
+ @ignore_paths = DEFAULT_IGNORE_PATHS
61
+ @ignore_paths |= options[:ignore_paths] if options[:ignore_paths]
62
+ @watch_all_modifications = options.fetch(:watch_all_modifications, false)
63
+
64
+ update_last_event
65
+ start_reactor
66
+ end
67
+
68
+ # Start the listener thread.
69
+ #
70
+ def start_reactor
71
+ return if ENV["GUARD_ENV"] == 'test'
72
+
73
+ Thread.new do
74
+ loop do
75
+ if @changed_files != [] && !@paused
76
+ changed_files = @changed_files.dup
77
+ clear_changed_files
78
+ ::Guard.run_on_change(changed_files)
79
+ else
80
+ sleep 0.1
81
+ end
82
+ end
83
+ end
84
+ end
85
+
86
+ # Start watching the root directory.
87
+ #
88
+ def start
89
+ watch(@directory)
90
+ timestamp_files
91
+ end
92
+
93
+ # Stop listening for events.
94
+ #
95
+ def stop
96
+ end
97
+
98
+ # Pause the listener to ignore change events.
99
+ #
100
+ def pause
101
+ @paused = true
102
+ end
103
+
104
+ # Unpause the listener to listen again to change events.
105
+ #
106
+ def run
107
+ @paused = false
108
+ end
109
+
110
+ # Clear the list of changed files.
111
+ #
112
+ def clear_changed_files
113
+ @changed_files.clear
114
+ end
115
+
116
+ # Store a listener callback.
117
+ #
118
+ # @param [Block] callback the callback to store
119
+ #
120
+ def on_change(&callback)
121
+ @callback = callback
122
+ end
123
+
124
+ # Updates the timestamp of the last event.
125
+ #
126
+ def update_last_event
127
+ @last_event = Time.now
128
+ end
129
+
130
+ # Get the modified files.
131
+ #
132
+ # If the `:watch_all_modifications` option is true, then moved and
133
+ # deleted files are also reported, but prefixed by an exclamation point.
134
+ #
135
+ # @example Deleted or moved file
136
+ # !/home/user/dir/file.rb
137
+ #
138
+ # @param [Array<String>] dirs the watched directories
139
+ # @param [Hash] options the listener options
140
+ # @option options [Symbol] all whether to files in sub directories
141
+ # @return [Array<String>] paths of files that have been modified
142
+ #
143
+ def modified_files(dirs, options = {})
144
+ last_event = @last_event
145
+ files = []
146
+ if @watch_all_modifications
147
+ deleted_files = @file_timestamp_hash.collect do |path, ts|
148
+ unless File.exists?(path)
149
+ @sha1_checksums_hash.delete(path)
150
+ @file_timestamp_hash.delete(path)
151
+ "!#{path}"
152
+ end
153
+ end
154
+ files.concat(deleted_files.compact)
155
+ end
156
+ update_last_event
157
+ files.concat(potentially_modified_files(dirs, options).select { |path| file_modified?(path, last_event) })
158
+
159
+ relativize_paths(files)
160
+ end
161
+
162
+ # Register a directory to watch.
163
+ # Must be implemented by the subclasses.
164
+ #
165
+ # @param [String] directory the directory to watch
166
+ #
167
+ def watch(directory)
168
+ raise NotImplementedError, "do whatever you want here, given the directory as only argument"
169
+ end
170
+
171
+ # Get all files that are in the watched directory.
172
+ #
173
+ # @return [Array<String>] the list of files
174
+ #
175
+ def all_files
176
+ potentially_modified_files([@directory], :all => true)
177
+ end
178
+
179
+ # Scopes all given paths to the current directory.
180
+ #
181
+ # @param [Array<String>] paths the paths to change
182
+ # @return [Array<String>] all paths now relative to the current dir
183
+ #
184
+ def relativize_paths(paths)
185
+ return paths unless relativize_paths?
186
+ paths.map do |path|
187
+ path.gsub(%r{^(!)?#{ @directory }/},'\1')
188
+ end
189
+ end
190
+
191
+ # Use paths relative to the current directory.
192
+ #
193
+ # @return [Boolean] whether to use relative or absolute paths
194
+ #
195
+ def relativize_paths?
196
+ !!@relativize_paths
197
+ end
198
+
199
+ # Populate initial timestamp file hash to watch for deleted or moved files.
200
+ #
201
+ def timestamp_files
202
+ all_files.each {|path| set_file_timestamp_hash(path, file_timestamp(path)) } if @watch_all_modifications
203
+ end
204
+
205
+ # Removes the ignored paths from the directory list.
206
+ #
207
+ # @param [Array<String>] dirs the directory to listen to
208
+ # @param [Array<String>] ignore_paths the paths to ignore
209
+ # @return children of the passed dirs that are not in the ignore_paths list
210
+ #
211
+ def exclude_ignored_paths(dirs, ignore_paths = self.ignore_paths)
212
+ Dir.glob(dirs.map { |d| "#{d.sub(%r{/+$}, '')}/*" }, File::FNM_DOTMATCH).reject do |path|
213
+ ignore_paths.include?(File.basename(path))
214
+ end
215
+ end
216
+
217
+ private
218
+
219
+ # Gets a list of files that are in the modified directories.
220
+ #
221
+ # @param [Array<String>] dirs the list of directories
222
+ # @param [Hash] options the find file option
223
+ # @option options [Symbol] all whether to files in sub directories
224
+ #
225
+ def potentially_modified_files(dirs, options = {})
226
+ paths = exclude_ignored_paths(dirs)
227
+
228
+ if options[:all]
229
+ paths.inject([]) do |array, path|
230
+ if File.file?(path)
231
+ array << path
232
+ else
233
+ array += Dir.glob("#{ path }/**/*", File::FNM_DOTMATCH).select { |p| File.file?(p) }
234
+ end
235
+ array
236
+ end
237
+ else
238
+ paths.select { |path| File.file?(path) }
239
+ end
240
+ end
241
+
242
+ # Test if the file content has changed.
243
+ #
244
+ # Depending on the filesystem, mtime/ctime is probably only precise to the second, so round
245
+ # both values down to the second for the comparison.
246
+ #
247
+ # ctime is used only on == comparison to always catches Rails 3.1 Assets pipelined on Mac OSX
248
+ #
249
+ # @param [String] path the file path
250
+ # @param [Time] last_event the time of the last event
251
+ # @return [Boolean] Whether the file content has changed or not.
252
+ #
253
+ def file_modified?(path, last_event)
254
+ ctime = File.ctime(path).to_i
255
+ mtime = File.mtime(path).to_i
256
+ if [mtime, ctime].max == last_event.to_i
257
+ file_content_modified?(path, sha1_checksum(path))
258
+ elsif mtime > last_event.to_i
259
+ set_sha1_checksums_hash(path, sha1_checksum(path))
260
+ true
261
+ elsif @watch_all_modifications
262
+ ts = file_timestamp(path)
263
+ if ts != @file_timestamp_hash[path]
264
+ set_file_timestamp_hash(path, ts)
265
+ true
266
+ end
267
+ else
268
+ false
269
+ end
270
+ rescue
271
+ false
272
+ end
273
+
274
+ # Tests if the file content has been modified by
275
+ # comparing the SHA1 checksum.
276
+ #
277
+ # @param [String] path the file path
278
+ # @param [String] sha1_checksum the checksum of the file
279
+ #
280
+ def file_content_modified?(path, sha1_checksum)
281
+ if @sha1_checksums_hash[path] != sha1_checksum
282
+ set_sha1_checksums_hash(path, sha1_checksum)
283
+ true
284
+ else
285
+ false
286
+ end
287
+ end
288
+
289
+ # Set save a files current timestamp
290
+ #
291
+ # @param [String] path the file path
292
+ # @param [Int] file_timestamp the files modified timestamp
293
+ #
294
+ def set_file_timestamp_hash(path, file_timestamp)
295
+ @file_timestamp_hash[path] = file_timestamp
296
+ end
297
+
298
+ # Set the current checksum of a file.
299
+ #
300
+ # @param [String] path the file path
301
+ # @param [String] sha1_checksum the checksum of the file
302
+ #
303
+ def set_sha1_checksums_hash(path, sha1_checksum)
304
+ @sha1_checksums_hash[path] = sha1_checksum
305
+ end
306
+
307
+ # Gets a files modified timestamp
308
+ #
309
+ # @path [String] path the file path
310
+ # @return [Int] file modified timestamp
311
+ #
312
+ def file_timestamp(path)
313
+ File.mtime(path).to_i
314
+ end
315
+
316
+ # Calculates the SHA1 checksum of a file.
317
+ #
318
+ # @param [String] path the path to the file
319
+ # @return [String] the SHA1 checksum
320
+ #
321
+ def sha1_checksum(path)
322
+ Digest::SHA1.file(path).to_s
323
+ end
324
+
325
+ # Test if the OS is Mac OS X.
326
+ #
327
+ # @return [Boolean] Whether the OS is Mac OS X
328
+ #
329
+ def self.mac?
330
+ RbConfig::CONFIG['target_os'] =~ /darwin/i
331
+ end
332
+
333
+ # Test if the OS is Linux.
334
+ #
335
+ # @return [Boolean] Whether the OS is Linux
336
+ #
337
+ def self.linux?
338
+ RbConfig::CONFIG['target_os'] =~ /linux/i
339
+ end
340
+
341
+ # Test if the OS is Windows.
342
+ #
343
+ # @return [Boolean] Whether the OS is Windows
344
+ #
345
+ def self.windows?
346
+ RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
347
+ end
348
+
349
+ end
350
+ end