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/CHANGELOG.md +348 -330
- data/LICENSE +19 -19
- data/README.md +464 -431
- data/bin/guard +6 -6
- data/lib/guard.rb +452 -414
- data/lib/guard/cli.rb +119 -178
- data/lib/guard/dsl.rb +370 -370
- data/lib/guard/dsl_describer.rb +150 -60
- data/lib/guard/group.rb +37 -37
- data/lib/guard/guard.rb +129 -113
- data/lib/guard/hook.rb +118 -118
- data/lib/guard/interactor.rb +110 -44
- data/lib/guard/listener.rb +350 -350
- data/lib/guard/listeners/darwin.rb +66 -66
- data/lib/guard/listeners/linux.rb +97 -97
- data/lib/guard/listeners/polling.rb +55 -55
- data/lib/guard/listeners/windows.rb +61 -61
- data/lib/guard/notifier.rb +290 -211
- data/lib/guard/templates/Guardfile +2 -2
- data/lib/guard/ui.rb +193 -188
- data/lib/guard/version.rb +6 -6
- data/lib/guard/watcher.rb +114 -110
- data/man/guard.1 +93 -93
- data/man/guard.1.html +176 -176
- metadata +107 -59
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
|
data/lib/guard/interactor.rb
CHANGED
@@ -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
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
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
|
data/lib/guard/listener.rb
CHANGED
@@ -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
|