listen 0.5.1 → 0.5.2
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 +173 -167
- data/LICENSE +20 -20
- data/lib/listen/adapters/polling.rb +1 -1
- data/lib/listen/directory_record.rb +344 -318
- data/lib/listen/listener.rb +203 -203
- data/lib/listen/multi_listener.rb +121 -121
- data/lib/listen/turnstile.rb +28 -28
- data/lib/listen/version.rb +3 -3
- metadata +11 -15
data/CHANGELOG.md
CHANGED
@@ -1,167 +1,173 @@
|
|
1
|
-
## 0.5.
|
2
|
-
|
3
|
-
### Bug
|
4
|
-
|
5
|
-
- [#
|
6
|
-
|
7
|
-
## 0.5.
|
8
|
-
|
9
|
-
###
|
10
|
-
|
11
|
-
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
###
|
16
|
-
|
17
|
-
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
###
|
22
|
-
|
23
|
-
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
- [#
|
32
|
-
|
33
|
-
## 0.4.
|
34
|
-
|
35
|
-
### Bug fix
|
36
|
-
|
37
|
-
- [#39](https://github.com/guard/listen/issues/39)
|
38
|
-
|
39
|
-
## 0.4.
|
40
|
-
|
41
|
-
### Bug
|
42
|
-
|
43
|
-
-
|
44
|
-
|
45
|
-
|
46
|
-
|
47
|
-
|
48
|
-
|
49
|
-
|
50
|
-
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
|
56
|
-
-
|
57
|
-
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
-
-
|
64
|
-
|
65
|
-
|
66
|
-
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
- [#
|
71
|
-
-
|
72
|
-
-
|
73
|
-
|
74
|
-
|
75
|
-
|
76
|
-
|
77
|
-
|
78
|
-
-
|
79
|
-
|
80
|
-
## 0.4.
|
81
|
-
|
82
|
-
###
|
83
|
-
|
84
|
-
-
|
85
|
-
|
86
|
-
|
87
|
-
|
88
|
-
|
89
|
-
|
90
|
-
|
91
|
-
|
92
|
-
|
93
|
-
-
|
94
|
-
-
|
95
|
-
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
###
|
105
|
-
|
106
|
-
-
|
107
|
-
|
108
|
-
## 0.3.
|
109
|
-
|
110
|
-
###
|
111
|
-
|
112
|
-
-
|
113
|
-
|
114
|
-
## 0.3.
|
115
|
-
|
116
|
-
###
|
117
|
-
|
118
|
-
-
|
119
|
-
|
120
|
-
## 0.3.
|
121
|
-
|
122
|
-
###
|
123
|
-
|
124
|
-
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
- Add
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
139
|
-
|
140
|
-
-
|
141
|
-
|
142
|
-
|
143
|
-
|
144
|
-
|
145
|
-
|
146
|
-
[
|
147
|
-
|
148
|
-
|
149
|
-
[#
|
150
|
-
[#
|
151
|
-
[#
|
152
|
-
[
|
153
|
-
[
|
154
|
-
[
|
155
|
-
[
|
156
|
-
[
|
157
|
-
[
|
158
|
-
[@
|
159
|
-
[@
|
160
|
-
[@
|
161
|
-
[@
|
162
|
-
[@
|
163
|
-
[@
|
164
|
-
[@
|
165
|
-
[@
|
166
|
-
[@
|
167
|
-
[
|
1
|
+
## 0.5.2 - Septemper 23, 2012
|
2
|
+
|
3
|
+
### Bug fix
|
4
|
+
|
5
|
+
- [#62] Fix double change callback with polling adapter. (fixed by [@thibaudgg][])
|
6
|
+
|
7
|
+
## 0.5.1 - Septemper 18, 2012
|
8
|
+
|
9
|
+
### Bug fix
|
10
|
+
|
11
|
+
- [#61] Fix a synchronisation bug that caused constant fallback to polling. (fixed by [@Maher4Ever][])
|
12
|
+
|
13
|
+
## 0.5.0 - Septemper 1, 2012
|
14
|
+
|
15
|
+
### New features
|
16
|
+
|
17
|
+
- Add a dependency manager to handle platform-specific gems. So there is no need anymore to install
|
18
|
+
extra gems which will never be used on the user system. ([@Maher4Ever][])
|
19
|
+
- Add a manual reporting mode to the adapters. ([@Maher4Ever][])
|
20
|
+
|
21
|
+
### Improvements
|
22
|
+
|
23
|
+
- [#28] Enhance the speed of detecting changes on Windows by using the [WDM][] library. ([@Maher4Ever][])
|
24
|
+
|
25
|
+
## 0.4.7 - June 27, 2012
|
26
|
+
|
27
|
+
### Bug fixes
|
28
|
+
|
29
|
+
- Increase latency to 0.25, to avoid useless polling fallback. (fixed by [@thibaudgg][])
|
30
|
+
- Change watched inotify events, to avoid duplication callback. (fixed by [@thibaudgg][])
|
31
|
+
- [#41](https://github.com/guard/listen/issues/41) Use lstat instead of stat when calculating mtime. (fixed by [@ebroder][])
|
32
|
+
|
33
|
+
## 0.4.6 - June 20, 2012
|
34
|
+
|
35
|
+
### Bug fix
|
36
|
+
|
37
|
+
- [#39](https://github.com/guard/listen/issues/39) Fix digest race condition. (fixed by [@dkubb][])
|
38
|
+
|
39
|
+
## 0.4.5 - June 13, 2012
|
40
|
+
|
41
|
+
### Bug fix
|
42
|
+
|
43
|
+
- [#39](https://github.com/guard/listen/issues/39) Rescue Errno::ENOENT when path inserted doesn't exist. (reported by [@textgoeshere][], fixed by [@thibaudgg][] and [@rymai][])
|
44
|
+
|
45
|
+
## 0.4.4 - June 8, 2012
|
46
|
+
|
47
|
+
### Bug fixes
|
48
|
+
|
49
|
+
- ~~[#39](https://github.com/guard/listen/issues/39) Non-existing path insertion bug. (reported by [@textgoeshere][], fixed by [@thibaudgg][])~~
|
50
|
+
- Fix relative path for directories containing special characters. (reported by [@napcs][], fixed by [@netzpirat][])
|
51
|
+
|
52
|
+
## 0.4.3 - June 6, 2012
|
53
|
+
|
54
|
+
### Bug fixes
|
55
|
+
|
56
|
+
- [#24](https://github.com/guard/listen/issues/24) Fail gracefully when the inotify limit is not enough for Listen to function. (reported by [@daemonza][], fixed by [@Maher4Ever][])
|
57
|
+
- [#32](https://github.com/guard/listen/issues/32) Fix a crash when trying to calculate the checksum of unreadable files. (reported by [@nex3][], fixed by [@Maher4Ever][])
|
58
|
+
|
59
|
+
### Improvements
|
60
|
+
|
61
|
+
- Add `#relative_paths` method to listeners. ([@Maher4Ever][])
|
62
|
+
- Add `#started?` query-method to adapters. ([@Maher4Ever][])
|
63
|
+
- Dynamically detect the mtime precision used on a system. ([@Maher4Ever][] with help from [@nex3][])
|
64
|
+
|
65
|
+
## 0.4.2 - May 1, 2012
|
66
|
+
|
67
|
+
### Bug fixes
|
68
|
+
|
69
|
+
- [#21](https://github.com/guard/listen/issues/21) Issues when listening to changes in relative paths. (reported by [@akerbos][], fixed by [@Maher4Ever][])
|
70
|
+
- [#27](https://github.com/guard/listen/issues/27) Wrong reports for files modifications. (reported by [@cobychapple][], fixed by [@Maher4Ever][])
|
71
|
+
- Fix segmentation fault when profiling on Windows. ([@Maher4Ever][])
|
72
|
+
- Fix redundant watchers on Windows. ([@Maher4Ever][])
|
73
|
+
|
74
|
+
### Improvements
|
75
|
+
|
76
|
+
- [#17](https://github.com/guard/listen/issues/17) Use regexp-patterns with the `ignore` method instead of supplying paths. (reported by [@fny][], added by [@Maher4Ever][])
|
77
|
+
- Speed improvement when listening to changes in directories with ignored paths. ([@Maher4Ever][])
|
78
|
+
- Added `.rbx` and `.svn` to ignored directories. ([@Maher4Ever][])
|
79
|
+
|
80
|
+
## 0.4.1 - April 15, 2012
|
81
|
+
|
82
|
+
### Bug fix
|
83
|
+
|
84
|
+
- [#18](https://github.com/guard/listen/issues/18) Listener crashes when removing directories with nested paths. (reported by [@daemonza][], fixed by [@Maher4Ever][])
|
85
|
+
|
86
|
+
## 0.4.0 - April 9, 2012
|
87
|
+
|
88
|
+
### New features
|
89
|
+
|
90
|
+
- Add `wait_for_callback` method to all adapters. ([@Maher4Ever][])
|
91
|
+
- Add `Listen::MultiListener` class to listen to multiple directories at once. ([@Maher4Ever][])
|
92
|
+
- Allow passing multiple directories to the `Listen.to` method. ([@Maher4Ever][])
|
93
|
+
- Add `blocking` option to `Listen#start` which can be used to disable blocking the current thread upon starting. ([@Maher4Ever][])
|
94
|
+
- Use absolute-paths in callbacks by default instead of relative-paths. ([@Maher4Ever][])
|
95
|
+
- Add `relative_paths` option to `Listen::Listener` to retain the old functionality. ([@Maher4Ever][])
|
96
|
+
|
97
|
+
### Improvements
|
98
|
+
|
99
|
+
- Encapsulate thread spawning in the linux-adapter. ([@Maher4Ever][])
|
100
|
+
- Encapsulate thread spawning in the darwin-adapter. ([@Maher4Ever][] with [@scottdavis][] help)
|
101
|
+
- Encapsulate thread spawning in the windows-adapter. ([@Maher4Ever][])
|
102
|
+
- Fix linux-adapter bug where Listen would report file-modification events on the parent-directory. ([@Maher4Ever][])
|
103
|
+
|
104
|
+
### Change
|
105
|
+
|
106
|
+
- Remove `wait_until_listening` as adapters doesn't need to run inside threads anymore ([@Maher4Ever][])
|
107
|
+
|
108
|
+
## 0.3.3 - March 6, 2012
|
109
|
+
|
110
|
+
### Improvement
|
111
|
+
|
112
|
+
- Improve pause/unpause. ([@thibaudgg][])
|
113
|
+
|
114
|
+
## 0.3.2 - March 4, 2012
|
115
|
+
|
116
|
+
### New feature
|
117
|
+
|
118
|
+
- Add pause/unpause listener's methods. ([@thibaudgg][])
|
119
|
+
|
120
|
+
## 0.3.1 - February 22, 2012
|
121
|
+
|
122
|
+
### Bug fix
|
123
|
+
|
124
|
+
- [#9](https://github.com/guard/listen/issues/9) Ignore doesn't seem to work. (reported by [@markiz][], fixed by [@thibaudgg][])
|
125
|
+
|
126
|
+
## 0.3.0 - February 21, 2012
|
127
|
+
|
128
|
+
### New features
|
129
|
+
|
130
|
+
- Add automatic fallback to polling if system adapter doesn't work (like a DropBox folder). ([@thibaudgg][])
|
131
|
+
- Add latency and force_polling options. ([@Maher4Ever][])
|
132
|
+
|
133
|
+
## 0.2.0 - February 13, 2012
|
134
|
+
|
135
|
+
### New features
|
136
|
+
|
137
|
+
- Add checksum comparaison support for detecting consecutive file modifications made during the same second. ([@thibaudgg][])
|
138
|
+
- Add rb-fchange support. ([@thibaudgg][])
|
139
|
+
- Add rb-inotify support. ([@thibaudgg][] with [@Maher4Ever][] help)
|
140
|
+
- Add rb-fsevent support. ([@thibaudgg][])
|
141
|
+
- Add non-recursive diff with multiple directories support. ([@thibaudgg][])
|
142
|
+
- Ignore .DS_Store by default. ([@thibaudgg][])
|
143
|
+
|
144
|
+
## 0.1.0 - January 28, 2012
|
145
|
+
|
146
|
+
- First version with only a polling adapter and basic features set (ignore & filter). ([@thibaudgg][])
|
147
|
+
|
148
|
+
<!--- The following link definition list is generated by PimpMyChangelog --->
|
149
|
+
[#9]: https://github.com/guard/listen/issues/9
|
150
|
+
[#17]: https://github.com/guard/listen/issues/17
|
151
|
+
[#18]: https://github.com/guard/listen/issues/18
|
152
|
+
[#21]: https://github.com/guard/listen/issues/21
|
153
|
+
[#24]: https://github.com/guard/listen/issues/24
|
154
|
+
[#27]: https://github.com/guard/listen/issues/27
|
155
|
+
[#28]: https://github.com/guard/listen/issues/28
|
156
|
+
[#32]: https://github.com/guard/listen/issues/32
|
157
|
+
[#39]: https://github.com/guard/listen/issues/39
|
158
|
+
[@Maher4Ever]: https://github.com/Maher4Ever
|
159
|
+
[@dkubb]: https://github.com/dkubb
|
160
|
+
[@ebroder]: https://github.com/ebroder
|
161
|
+
[@akerbos]: https://github.com/akerbos
|
162
|
+
[@cobychapple]: https://github.com/cobychapple
|
163
|
+
[@daemonza]: https://github.com/daemonza
|
164
|
+
[@fny]: https://github.com/fny
|
165
|
+
[@markiz]: https://github.com/markiz
|
166
|
+
[@napcs]: https://github.com/napcs
|
167
|
+
[@netzpirat]: https://github.com/netzpirat
|
168
|
+
[@nex3]: https://github.com/nex3
|
169
|
+
[@rymai]: https://github.com/rymai
|
170
|
+
[@scottdavis]: https://github.com/scottdavis
|
171
|
+
[@textgoeshere]: https://github.com/textgoeshere
|
172
|
+
[@thibaudgg]: https://github.com/thibaudgg
|
173
|
+
[WDM]: https://github.com/Maher4Ever/wdm
|
data/LICENSE
CHANGED
@@ -1,20 +1,20 @@
|
|
1
|
-
Copyright (c) 2012 Thibaud Guillaume-Gentil
|
2
|
-
|
3
|
-
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
-
a copy of this software and associated documentation files (the
|
5
|
-
"Software"), to deal in the Software without restriction, including
|
6
|
-
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
-
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
-
permit persons to whom the Software is furnished to do so, subject to
|
9
|
-
the following conditions:
|
10
|
-
|
11
|
-
The above copyright notice and this permission notice shall be
|
12
|
-
included in all copies or substantial portions of the Software.
|
13
|
-
|
14
|
-
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
-
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
-
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
-
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
-
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
-
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
-
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
1
|
+
Copyright (c) 2012 Thibaud Guillaume-Gentil
|
2
|
+
|
3
|
+
Permission is hereby granted, free of charge, to any person obtaining
|
4
|
+
a copy of this software and associated documentation files (the
|
5
|
+
"Software"), to deal in the Software without restriction, including
|
6
|
+
without limitation the rights to use, copy, modify, merge, publish,
|
7
|
+
distribute, sublicense, and/or sell copies of the Software, and to
|
8
|
+
permit persons to whom the Software is furnished to do so, subject to
|
9
|
+
the following conditions:
|
10
|
+
|
11
|
+
The above copyright notice and this permission notice shall be
|
12
|
+
included in all copies or substantial portions of the Software.
|
13
|
+
|
14
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
15
|
+
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
|
16
|
+
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
|
17
|
+
NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
|
18
|
+
LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
|
19
|
+
OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
|
20
|
+
WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
@@ -1,318 +1,344 @@
|
|
1
|
-
require 'set'
|
2
|
-
require 'find'
|
3
|
-
require 'digest/sha1'
|
4
|
-
|
5
|
-
module Listen
|
6
|
-
|
7
|
-
# The directory record stores information about
|
8
|
-
# a directory and keeps track of changes to
|
9
|
-
# the structure of its childs.
|
10
|
-
#
|
11
|
-
class DirectoryRecord
|
12
|
-
attr_reader :directory, :paths, :sha1_checksums
|
13
|
-
|
14
|
-
DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
|
15
|
-
|
16
|
-
DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
|
17
|
-
|
18
|
-
# Defines the used precision based on the type of mtime returned by the
|
19
|
-
# system (whether its in milliseconds or just seconds)
|
20
|
-
#
|
21
|
-
HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
|
22
|
-
|
23
|
-
# Data structure used to save meta data about a path
|
24
|
-
#
|
25
|
-
MetaData = Struct.new(:type, :mtime)
|
26
|
-
|
27
|
-
# Class methods
|
28
|
-
#
|
29
|
-
class << self
|
30
|
-
|
31
|
-
# Creates the ignoring patterns from the default ignored
|
32
|
-
# directories and extensions. It memoizes the generated patterns
|
33
|
-
# to avoid unnecessary computation.
|
34
|
-
#
|
35
|
-
def generate_default_ignoring_patterns
|
36
|
-
@@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
|
37
|
-
# Add directories
|
38
|
-
ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
|
39
|
-
default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
|
40
|
-
|
41
|
-
# Add extensions
|
42
|
-
ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
|
43
|
-
default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
|
44
|
-
end
|
45
|
-
end
|
46
|
-
end
|
47
|
-
|
48
|
-
# Initializes a directory record.
|
49
|
-
#
|
50
|
-
# @option [String] directory the directory to keep track of
|
51
|
-
#
|
52
|
-
def initialize(directory)
|
53
|
-
raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
|
54
|
-
|
55
|
-
@directory = directory
|
56
|
-
@ignoring_patterns = Set.new
|
57
|
-
@filtering_patterns = Set.new
|
58
|
-
@sha1_checksums = Hash.new
|
59
|
-
|
60
|
-
@ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
|
61
|
-
end
|
62
|
-
|
63
|
-
# Returns the ignoring patterns in the record
|
64
|
-
#
|
65
|
-
# @return [Array<Regexp>] the ignoring patterns
|
66
|
-
#
|
67
|
-
def ignoring_patterns
|
68
|
-
@ignoring_patterns.to_a
|
69
|
-
end
|
70
|
-
|
71
|
-
# Returns the filtering patterns used in the record to know
|
72
|
-
# which paths should be stored.
|
73
|
-
#
|
74
|
-
# @return [Array<Regexp>] the filtering patterns
|
75
|
-
#
|
76
|
-
def filtering_patterns
|
77
|
-
@filtering_patterns.to_a
|
78
|
-
end
|
79
|
-
|
80
|
-
# Adds ignoring patterns to the record.
|
81
|
-
#
|
82
|
-
# @example Ignore some paths
|
83
|
-
# ignore %r{^ignored/path/}, /man/
|
84
|
-
#
|
85
|
-
# @param [Regexp] regexp a pattern for ignoring paths
|
86
|
-
#
|
87
|
-
def ignore(*regexps)
|
88
|
-
@ignoring_patterns.merge(regexps)
|
89
|
-
end
|
90
|
-
|
91
|
-
# Adds filtering patterns to the listener.
|
92
|
-
#
|
93
|
-
# @example Filter some files
|
94
|
-
# ignore /\.txt$/, /.*\.zip/
|
95
|
-
#
|
96
|
-
# @param [Regexp] regexp a pattern for filtering paths
|
97
|
-
#
|
98
|
-
def filter(*regexps)
|
99
|
-
@filtering_patterns.merge(regexps)
|
100
|
-
end
|
101
|
-
|
102
|
-
# Returns whether a path should be ignored or not.
|
103
|
-
#
|
104
|
-
# @param [String] path the path to test.
|
105
|
-
#
|
106
|
-
# @return [Boolean]
|
107
|
-
#
|
108
|
-
def ignored?(path)
|
109
|
-
path = relative_to_base(path)
|
110
|
-
@ignoring_patterns.any? { |pattern| pattern =~ path }
|
111
|
-
end
|
112
|
-
|
113
|
-
# Returns whether a path should be filtered or not.
|
114
|
-
#
|
115
|
-
# @param [String] path the path to test.
|
116
|
-
#
|
117
|
-
# @return [Boolean]
|
118
|
-
#
|
119
|
-
def filtered?(path)
|
120
|
-
# When no filtering patterns are set, ALL files are stored.
|
121
|
-
return true if @filtering_patterns.empty?
|
122
|
-
|
123
|
-
path = relative_to_base(path)
|
124
|
-
@filtering_patterns.any? { |pattern| pattern =~ path }
|
125
|
-
end
|
126
|
-
|
127
|
-
# Finds the paths that should be stored and adds them
|
128
|
-
# to the paths' hash.
|
129
|
-
#
|
130
|
-
def build
|
131
|
-
@paths = Hash.new { |h, k| h[k] = Hash.new }
|
132
|
-
important_paths { |path| insert_path(path) }
|
133
|
-
end
|
134
|
-
|
135
|
-
# Detects changes in the passed directories, updates
|
136
|
-
# the record with the new changes and returns the changes
|
137
|
-
#
|
138
|
-
# @param [Array] directories the list of directories scan for changes
|
139
|
-
# @param [Hash] options
|
140
|
-
# @option options [Boolean] recursive scan all sub-directories recursively
|
141
|
-
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
142
|
-
#
|
143
|
-
# @return [Hash<Array>] the changes
|
144
|
-
#
|
145
|
-
def fetch_changes(directories, options = {})
|
146
|
-
@changes = { :modified => [], :added => [], :removed => [] }
|
147
|
-
directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
|
148
|
-
|
149
|
-
directories.each do |directory|
|
150
|
-
next unless directory[@directory] # Path is or inside directory
|
151
|
-
detect_modifications_and_removals(directory, options)
|
152
|
-
detect_additions(directory, options)
|
153
|
-
end
|
154
|
-
|
155
|
-
@changes
|
156
|
-
end
|
157
|
-
|
158
|
-
# Converts an absolute path to a path that's relative to the base directory.
|
159
|
-
#
|
160
|
-
# @param [String] path the path to convert
|
161
|
-
#
|
162
|
-
# @return [String] the relative path
|
163
|
-
#
|
164
|
-
def relative_to_base(path)
|
165
|
-
return nil unless path[@directory]
|
166
|
-
path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '')
|
167
|
-
end
|
168
|
-
|
169
|
-
private
|
170
|
-
|
171
|
-
# Detects modifications and removals recursively in a directory.
|
172
|
-
#
|
173
|
-
# @note Modifications detection begins by checking the modification time (mtime)
|
174
|
-
# of files and then by checking content changes (using SHA1-checksum)
|
175
|
-
# when the mtime of files is not changed.
|
176
|
-
#
|
177
|
-
# @param [String] directory the path to analyze
|
178
|
-
# @param [Hash] options
|
179
|
-
# @option options [Boolean] recursive scan all sub-directories recursively
|
180
|
-
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
181
|
-
#
|
182
|
-
def detect_modifications_and_removals(directory, options = {})
|
183
|
-
@paths[directory].each do |basename, meta_data|
|
184
|
-
path = File.join(directory, basename)
|
185
|
-
|
186
|
-
case meta_data.type
|
187
|
-
when 'Dir'
|
188
|
-
if File.directory?(path)
|
189
|
-
detect_modifications_and_removals(path, options) if options[:recursive]
|
190
|
-
else
|
191
|
-
detect_modifications_and_removals(path, { :recursive => true }.merge(options))
|
192
|
-
@paths[directory].delete(basename)
|
193
|
-
@paths.delete("#{directory}/#{basename}")
|
194
|
-
end
|
195
|
-
when 'File'
|
196
|
-
if File.exist?(path)
|
197
|
-
new_mtime = mtime_of(path)
|
198
|
-
|
199
|
-
# First check if we are in the same second (to update checksums)
|
200
|
-
# before checking the time difference
|
201
|
-
if
|
202
|
-
# Update the
|
203
|
-
|
204
|
-
|
205
|
-
|
206
|
-
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
216
|
-
|
217
|
-
|
218
|
-
|
219
|
-
|
220
|
-
#
|
221
|
-
#
|
222
|
-
# @
|
223
|
-
#
|
224
|
-
|
225
|
-
|
226
|
-
|
227
|
-
|
228
|
-
|
229
|
-
|
230
|
-
|
231
|
-
|
232
|
-
|
233
|
-
|
234
|
-
|
235
|
-
|
236
|
-
|
237
|
-
|
238
|
-
|
239
|
-
|
240
|
-
|
241
|
-
|
242
|
-
|
243
|
-
|
244
|
-
|
245
|
-
|
246
|
-
|
247
|
-
|
248
|
-
|
249
|
-
|
250
|
-
|
251
|
-
#
|
252
|
-
#
|
253
|
-
|
254
|
-
|
255
|
-
|
256
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
@sha1_checksums[path]
|
261
|
-
|
262
|
-
|
263
|
-
|
264
|
-
|
265
|
-
|
266
|
-
|
267
|
-
|
268
|
-
|
269
|
-
|
270
|
-
|
271
|
-
|
272
|
-
|
273
|
-
|
274
|
-
|
275
|
-
|
276
|
-
|
277
|
-
|
278
|
-
|
279
|
-
|
280
|
-
|
281
|
-
|
282
|
-
|
283
|
-
|
284
|
-
|
285
|
-
|
286
|
-
|
287
|
-
|
288
|
-
|
289
|
-
#
|
290
|
-
|
291
|
-
|
292
|
-
|
293
|
-
|
294
|
-
|
295
|
-
|
296
|
-
|
297
|
-
|
298
|
-
|
299
|
-
|
300
|
-
|
301
|
-
|
302
|
-
|
303
|
-
|
304
|
-
|
305
|
-
|
306
|
-
|
307
|
-
|
308
|
-
|
309
|
-
|
310
|
-
|
311
|
-
|
312
|
-
#
|
313
|
-
#
|
314
|
-
|
315
|
-
|
316
|
-
|
317
|
-
|
318
|
-
|
1
|
+
require 'set'
|
2
|
+
require 'find'
|
3
|
+
require 'digest/sha1'
|
4
|
+
|
5
|
+
module Listen
|
6
|
+
|
7
|
+
# The directory record stores information about
|
8
|
+
# a directory and keeps track of changes to
|
9
|
+
# the structure of its childs.
|
10
|
+
#
|
11
|
+
class DirectoryRecord
|
12
|
+
attr_reader :directory, :paths, :sha1_checksums
|
13
|
+
|
14
|
+
DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn log tmp vendor]
|
15
|
+
|
16
|
+
DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
|
17
|
+
|
18
|
+
# Defines the used precision based on the type of mtime returned by the
|
19
|
+
# system (whether its in milliseconds or just seconds)
|
20
|
+
#
|
21
|
+
HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
|
22
|
+
|
23
|
+
# Data structure used to save meta data about a path
|
24
|
+
#
|
25
|
+
MetaData = Struct.new(:type, :mtime)
|
26
|
+
|
27
|
+
# Class methods
|
28
|
+
#
|
29
|
+
class << self
|
30
|
+
|
31
|
+
# Creates the ignoring patterns from the default ignored
|
32
|
+
# directories and extensions. It memoizes the generated patterns
|
33
|
+
# to avoid unnecessary computation.
|
34
|
+
#
|
35
|
+
def generate_default_ignoring_patterns
|
36
|
+
@@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
|
37
|
+
# Add directories
|
38
|
+
ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
|
39
|
+
default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
|
40
|
+
|
41
|
+
# Add extensions
|
42
|
+
ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
|
43
|
+
default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
# Initializes a directory record.
|
49
|
+
#
|
50
|
+
# @option [String] directory the directory to keep track of
|
51
|
+
#
|
52
|
+
def initialize(directory)
|
53
|
+
raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
|
54
|
+
|
55
|
+
@directory = directory
|
56
|
+
@ignoring_patterns = Set.new
|
57
|
+
@filtering_patterns = Set.new
|
58
|
+
@sha1_checksums = Hash.new
|
59
|
+
|
60
|
+
@ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
|
61
|
+
end
|
62
|
+
|
63
|
+
# Returns the ignoring patterns in the record
|
64
|
+
#
|
65
|
+
# @return [Array<Regexp>] the ignoring patterns
|
66
|
+
#
|
67
|
+
def ignoring_patterns
|
68
|
+
@ignoring_patterns.to_a
|
69
|
+
end
|
70
|
+
|
71
|
+
# Returns the filtering patterns used in the record to know
|
72
|
+
# which paths should be stored.
|
73
|
+
#
|
74
|
+
# @return [Array<Regexp>] the filtering patterns
|
75
|
+
#
|
76
|
+
def filtering_patterns
|
77
|
+
@filtering_patterns.to_a
|
78
|
+
end
|
79
|
+
|
80
|
+
# Adds ignoring patterns to the record.
|
81
|
+
#
|
82
|
+
# @example Ignore some paths
|
83
|
+
# ignore %r{^ignored/path/}, /man/
|
84
|
+
#
|
85
|
+
# @param [Regexp] regexp a pattern for ignoring paths
|
86
|
+
#
|
87
|
+
def ignore(*regexps)
|
88
|
+
@ignoring_patterns.merge(regexps)
|
89
|
+
end
|
90
|
+
|
91
|
+
# Adds filtering patterns to the listener.
|
92
|
+
#
|
93
|
+
# @example Filter some files
|
94
|
+
# ignore /\.txt$/, /.*\.zip/
|
95
|
+
#
|
96
|
+
# @param [Regexp] regexp a pattern for filtering paths
|
97
|
+
#
|
98
|
+
def filter(*regexps)
|
99
|
+
@filtering_patterns.merge(regexps)
|
100
|
+
end
|
101
|
+
|
102
|
+
# Returns whether a path should be ignored or not.
|
103
|
+
#
|
104
|
+
# @param [String] path the path to test.
|
105
|
+
#
|
106
|
+
# @return [Boolean]
|
107
|
+
#
|
108
|
+
def ignored?(path)
|
109
|
+
path = relative_to_base(path)
|
110
|
+
@ignoring_patterns.any? { |pattern| pattern =~ path }
|
111
|
+
end
|
112
|
+
|
113
|
+
# Returns whether a path should be filtered or not.
|
114
|
+
#
|
115
|
+
# @param [String] path the path to test.
|
116
|
+
#
|
117
|
+
# @return [Boolean]
|
118
|
+
#
|
119
|
+
def filtered?(path)
|
120
|
+
# When no filtering patterns are set, ALL files are stored.
|
121
|
+
return true if @filtering_patterns.empty?
|
122
|
+
|
123
|
+
path = relative_to_base(path)
|
124
|
+
@filtering_patterns.any? { |pattern| pattern =~ path }
|
125
|
+
end
|
126
|
+
|
127
|
+
# Finds the paths that should be stored and adds them
|
128
|
+
# to the paths' hash.
|
129
|
+
#
|
130
|
+
def build
|
131
|
+
@paths = Hash.new { |h, k| h[k] = Hash.new }
|
132
|
+
important_paths { |path| insert_path(path) }
|
133
|
+
end
|
134
|
+
|
135
|
+
# Detects changes in the passed directories, updates
|
136
|
+
# the record with the new changes and returns the changes
|
137
|
+
#
|
138
|
+
# @param [Array] directories the list of directories scan for changes
|
139
|
+
# @param [Hash] options
|
140
|
+
# @option options [Boolean] recursive scan all sub-directories recursively
|
141
|
+
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
142
|
+
#
|
143
|
+
# @return [Hash<Array>] the changes
|
144
|
+
#
|
145
|
+
def fetch_changes(directories, options = {})
|
146
|
+
@changes = { :modified => [], :added => [], :removed => [] }
|
147
|
+
directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
|
148
|
+
|
149
|
+
directories.each do |directory|
|
150
|
+
next unless directory[@directory] # Path is or inside directory
|
151
|
+
detect_modifications_and_removals(directory, options)
|
152
|
+
detect_additions(directory, options)
|
153
|
+
end
|
154
|
+
|
155
|
+
@changes
|
156
|
+
end
|
157
|
+
|
158
|
+
# Converts an absolute path to a path that's relative to the base directory.
|
159
|
+
#
|
160
|
+
# @param [String] path the path to convert
|
161
|
+
#
|
162
|
+
# @return [String] the relative path
|
163
|
+
#
|
164
|
+
def relative_to_base(path)
|
165
|
+
return nil unless path[@directory]
|
166
|
+
path.sub(%r{^#{Regexp.quote(@directory)}#{File::SEPARATOR}?}, '')
|
167
|
+
end
|
168
|
+
|
169
|
+
private
|
170
|
+
|
171
|
+
# Detects modifications and removals recursively in a directory.
|
172
|
+
#
|
173
|
+
# @note Modifications detection begins by checking the modification time (mtime)
|
174
|
+
# of files and then by checking content changes (using SHA1-checksum)
|
175
|
+
# when the mtime of files is not changed.
|
176
|
+
#
|
177
|
+
# @param [String] directory the path to analyze
|
178
|
+
# @param [Hash] options
|
179
|
+
# @option options [Boolean] recursive scan all sub-directories recursively
|
180
|
+
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
181
|
+
#
|
182
|
+
def detect_modifications_and_removals(directory, options = {})
|
183
|
+
@paths[directory].each do |basename, meta_data|
|
184
|
+
path = File.join(directory, basename)
|
185
|
+
|
186
|
+
case meta_data.type
|
187
|
+
when 'Dir'
|
188
|
+
if File.directory?(path)
|
189
|
+
detect_modifications_and_removals(path, options) if options[:recursive]
|
190
|
+
else
|
191
|
+
detect_modifications_and_removals(path, { :recursive => true }.merge(options))
|
192
|
+
@paths[directory].delete(basename)
|
193
|
+
@paths.delete("#{directory}/#{basename}")
|
194
|
+
end
|
195
|
+
when 'File'
|
196
|
+
if File.exist?(path)
|
197
|
+
new_mtime = mtime_of(path)
|
198
|
+
|
199
|
+
# First check if we are in the same second (to update checksums)
|
200
|
+
# before checking the time difference
|
201
|
+
if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
|
202
|
+
# Update the sha1 checksum of the file
|
203
|
+
insert_sha1_checksum(path)
|
204
|
+
|
205
|
+
# Update the meta data of the file
|
206
|
+
meta_data.mtime = new_mtime
|
207
|
+
@paths[directory][basename] = meta_data
|
208
|
+
|
209
|
+
@changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
|
210
|
+
end
|
211
|
+
else
|
212
|
+
@paths[directory].delete(basename)
|
213
|
+
@sha1_checksums.delete(path)
|
214
|
+
@changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
end
|
219
|
+
|
220
|
+
# Detects additions in a directory.
|
221
|
+
#
|
222
|
+
# @param [String] directory the path to analyze
|
223
|
+
# @param [Hash] options
|
224
|
+
# @option options [Boolean] recursive scan all sub-directories recursively
|
225
|
+
# @option options [Boolean] relative_paths whether or not to use relative paths for changes
|
226
|
+
#
|
227
|
+
def detect_additions(directory, options = {})
|
228
|
+
# Don't process removed directories
|
229
|
+
return unless File.exist?(directory)
|
230
|
+
|
231
|
+
Find.find(directory) do |path|
|
232
|
+
next if path == @directory
|
233
|
+
|
234
|
+
if File.directory?(path)
|
235
|
+
# Add a trailing slash to directories when checking if a directory is
|
236
|
+
# ignored to optimize finding them as Find.find doesn't.
|
237
|
+
if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path)))
|
238
|
+
Find.prune # Don't look any further into this directory.
|
239
|
+
else
|
240
|
+
insert_path(path)
|
241
|
+
end
|
242
|
+
elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
|
243
|
+
if File.file?(path)
|
244
|
+
@changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
|
245
|
+
insert_path(path)
|
246
|
+
end
|
247
|
+
end
|
248
|
+
end
|
249
|
+
end
|
250
|
+
|
251
|
+
# Returns whether or not a file's content has been modified by
|
252
|
+
# comparing the SHA1-checksum to a stored one.
|
253
|
+
# Ensure that the SHA1-checksum is inserted to the sha1_checksums
|
254
|
+
# array for later comparaison if false.
|
255
|
+
#
|
256
|
+
# @param [String] path the file path
|
257
|
+
#
|
258
|
+
def content_modified?(path)
|
259
|
+
@sha1_checksum = sha1_checksum(path)
|
260
|
+
if @sha1_checksums[path] == @sha1_checksum || !@sha1_checksums.key?(path)
|
261
|
+
insert_sha1_checksum(path)
|
262
|
+
false
|
263
|
+
else
|
264
|
+
true
|
265
|
+
end
|
266
|
+
end
|
267
|
+
|
268
|
+
# Inserts a SHA1-checksum path in @SHA1-checksums hash.
|
269
|
+
#
|
270
|
+
# @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
|
271
|
+
#
|
272
|
+
def insert_sha1_checksum(path)
|
273
|
+
if @sha1_checksum ||= sha1_checksum(path)
|
274
|
+
@sha1_checksums[path] = @sha1_checksum
|
275
|
+
@sha1_checksum = nil
|
276
|
+
end
|
277
|
+
end
|
278
|
+
|
279
|
+
# Returns the SHA1-checksum for the file path.
|
280
|
+
#
|
281
|
+
# @param [String] path the file path
|
282
|
+
#
|
283
|
+
def sha1_checksum(path)
|
284
|
+
Digest::SHA1.file(path).to_s
|
285
|
+
rescue Errno::EACCES, Errno::ENOENT
|
286
|
+
nil
|
287
|
+
end
|
288
|
+
|
289
|
+
# Traverses the base directory looking for paths that should
|
290
|
+
# be stored; thus paths that are filters or not ignored.
|
291
|
+
#
|
292
|
+
# @yield [path] an important path
|
293
|
+
#
|
294
|
+
def important_paths
|
295
|
+
Find.find(@directory) do |path|
|
296
|
+
next if path == @directory
|
297
|
+
|
298
|
+
if File.directory?(path)
|
299
|
+
# Add a trailing slash to directories when checking if a directory is
|
300
|
+
# ignored to optimize finding them as Find.find doesn't.
|
301
|
+
if ignored?(path + File::SEPARATOR)
|
302
|
+
Find.prune # Don't look any further into this directory.
|
303
|
+
else
|
304
|
+
yield(path)
|
305
|
+
end
|
306
|
+
elsif !ignored?(path) && filtered?(path)
|
307
|
+
yield(path)
|
308
|
+
end
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
# Inserts a path with its type (Dir or File) in paths hash.
|
313
|
+
#
|
314
|
+
# @param [String] path the path to insert in @paths.
|
315
|
+
#
|
316
|
+
def insert_path(path)
|
317
|
+
meta_data = MetaData.new
|
318
|
+
meta_data.type = File.directory?(path) ? 'Dir' : 'File'
|
319
|
+
meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet
|
320
|
+
@paths[File.dirname(path)][File.basename(path)] = meta_data
|
321
|
+
rescue Errno::ENOENT
|
322
|
+
end
|
323
|
+
|
324
|
+
# Returns whether or not a path exists in the paths hash.
|
325
|
+
#
|
326
|
+
# @param [String] path the path to check
|
327
|
+
#
|
328
|
+
# @return [Boolean]
|
329
|
+
#
|
330
|
+
def existing_path?(path)
|
331
|
+
@paths[File.dirname(path)][File.basename(path)] != nil
|
332
|
+
end
|
333
|
+
|
334
|
+
# Returns the modification time of a file based on the precision defined by the system
|
335
|
+
#
|
336
|
+
# @param [String] file the file for which the mtime must be returned
|
337
|
+
#
|
338
|
+
# @return [Fixnum, Float] the mtime of the file
|
339
|
+
#
|
340
|
+
def mtime_of(file)
|
341
|
+
File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i)
|
342
|
+
end
|
343
|
+
end
|
344
|
+
end
|