sass 3.4.25 → 3.5.0.pre.rc.1

Sign up to get free protection for your applications and to get access to all the features.
Files changed (156) hide show
  1. checksums.yaml +4 -4
  2. data/.yardopts +1 -1
  3. data/README.md +1 -1
  4. data/Rakefile +13 -157
  5. data/VERSION +1 -1
  6. data/VERSION_DATE +1 -1
  7. data/VERSION_NAME +1 -1
  8. data/lib/sass.rb +3 -10
  9. data/lib/sass/cache_stores/filesystem.rb +1 -1
  10. data/lib/sass/css.rb +2 -3
  11. data/lib/sass/engine.rb +46 -32
  12. data/lib/sass/environment.rb +27 -6
  13. data/lib/sass/error.rb +5 -5
  14. data/lib/sass/exec/base.rb +3 -12
  15. data/lib/sass/features.rb +1 -0
  16. data/lib/sass/importers/filesystem.rb +2 -2
  17. data/lib/sass/plugin.rb +1 -1
  18. data/lib/sass/plugin/compiler.rb +21 -51
  19. data/lib/sass/plugin/configuration.rb +1 -1
  20. data/lib/sass/plugin/rack.rb +3 -3
  21. data/lib/sass/plugin/staleness_checker.rb +3 -3
  22. data/lib/sass/railtie.rb +1 -1
  23. data/lib/sass/script.rb +3 -3
  24. data/lib/sass/script/functions.rb +238 -47
  25. data/lib/sass/script/lexer.rb +8 -6
  26. data/lib/sass/script/parser.rb +76 -75
  27. data/lib/sass/script/tree/funcall.rb +35 -30
  28. data/lib/sass/script/tree/list_literal.rb +23 -8
  29. data/lib/sass/script/tree/map_literal.rb +2 -2
  30. data/lib/sass/script/tree/node.rb +2 -10
  31. data/lib/sass/script/tree/operation.rb +16 -50
  32. data/lib/sass/script/value.rb +2 -0
  33. data/lib/sass/script/value/arg_list.rb +1 -1
  34. data/lib/sass/script/value/base.rb +20 -3
  35. data/lib/sass/script/value/callable.rb +25 -0
  36. data/lib/sass/script/value/color.rb +10 -10
  37. data/lib/sass/script/value/function.rb +19 -0
  38. data/lib/sass/script/value/helpers.rb +16 -7
  39. data/lib/sass/script/value/list.rb +33 -12
  40. data/lib/sass/script/value/map.rb +2 -2
  41. data/lib/sass/script/value/number.rb +3 -3
  42. data/lib/sass/script/value/string.rb +12 -5
  43. data/lib/sass/scss/parser.rb +101 -45
  44. data/lib/sass/scss/rx.rb +5 -11
  45. data/lib/sass/scss/static_parser.rb +0 -7
  46. data/lib/sass/selector.rb +4 -0
  47. data/lib/sass/selector/abstract_sequence.rb +5 -5
  48. data/lib/sass/selector/comma_sequence.rb +3 -15
  49. data/lib/sass/selector/pseudo.rb +4 -0
  50. data/lib/sass/selector/sequence.rb +30 -3
  51. data/lib/sass/selector/simple.rb +13 -7
  52. data/lib/sass/selector/simple_sequence.rb +1 -1
  53. data/lib/sass/shared.rb +3 -5
  54. data/lib/sass/source/map.rb +4 -4
  55. data/lib/sass/source/position.rb +4 -4
  56. data/lib/sass/stack.rb +21 -1
  57. data/lib/sass/tree/charset_node.rb +1 -1
  58. data/lib/sass/tree/comment_node.rb +1 -1
  59. data/lib/sass/tree/node.rb +3 -3
  60. data/lib/sass/tree/prop_node.rb +46 -54
  61. data/lib/sass/tree/rule_node.rb +7 -15
  62. data/lib/sass/tree/visitors/check_nesting.rb +1 -1
  63. data/lib/sass/tree/visitors/convert.rb +2 -3
  64. data/lib/sass/tree/visitors/cssize.rb +1 -10
  65. data/lib/sass/tree/visitors/deep_copy.rb +2 -2
  66. data/lib/sass/tree/visitors/perform.rb +23 -12
  67. data/lib/sass/tree/visitors/set_options.rb +1 -1
  68. data/lib/sass/tree/visitors/to_css.rb +46 -12
  69. data/lib/sass/util.rb +16 -321
  70. data/lib/sass/util/multibyte_string_scanner.rb +127 -131
  71. data/lib/sass/util/normalized_map.rb +1 -8
  72. data/lib/sass/version.rb +2 -2
  73. data/test/sass-spec.yml +1 -1
  74. data/test/sass/compiler_test.rb +4 -14
  75. data/test/sass/conversion_test.rb +113 -162
  76. data/test/sass/css2sass_test.rb +17 -19
  77. data/test/sass/css_variable_test.rb +176 -70
  78. data/test/sass/encoding_test.rb +2 -32
  79. data/test/sass/engine_test.rb +114 -65
  80. data/test/sass/extend_test.rb +37 -51
  81. data/test/sass/functions_test.rb +57 -15
  82. data/test/sass/importer_test.rb +2 -2
  83. data/test/sass/more_templates/more1.sass +10 -10
  84. data/test/sass/more_templates/more_import.sass +2 -2
  85. data/test/sass/plugin_test.rb +9 -12
  86. data/test/sass/script_conversion_test.rb +9 -0
  87. data/test/sass/script_test.rb +38 -48
  88. data/test/sass/scss/css_test.rb +5 -19
  89. data/test/sass/scss/scss_test.rb +58 -39
  90. data/test/sass/source_map_test.rb +26 -28
  91. data/test/sass/templates/_partial.sass +1 -1
  92. data/test/sass/templates/basic.sass +10 -10
  93. data/test/sass/templates/bork1.sass +1 -1
  94. data/test/sass/templates/bork5.sass +1 -1
  95. data/test/sass/templates/compact.sass +10 -10
  96. data/test/sass/templates/complex.sass +187 -187
  97. data/test/sass/templates/compressed.sass +10 -10
  98. data/test/sass/templates/expanded.sass +10 -10
  99. data/test/sass/templates/import.sass +2 -2
  100. data/test/sass/templates/importee.sass +3 -3
  101. data/test/sass/templates/mixins.sass +22 -22
  102. data/test/sass/templates/multiline.sass +4 -4
  103. data/test/sass/templates/nested.sass +13 -13
  104. data/test/sass/templates/parent_ref.sass +12 -12
  105. data/test/sass/templates/script.sass +70 -70
  106. data/test/sass/templates/subdir/nested_subdir/_nested_partial.sass +1 -1
  107. data/test/sass/templates/subdir/nested_subdir/nested_subdir.sass +2 -2
  108. data/test/sass/templates/subdir/subdir.sass +3 -3
  109. data/test/sass/templates/units.sass +10 -10
  110. data/test/sass/util/multibyte_string_scanner_test.rb +139 -149
  111. data/test/sass/util_test.rb +0 -36
  112. data/test/test_helper.rb +39 -0
  113. metadata +30 -57
  114. data/extra/sass-spec-ref.sh +0 -32
  115. data/lib/sass/deprecation.rb +0 -55
  116. data/lib/sass/script/css_variable_warning.rb +0 -52
  117. data/lib/sass/util/cross_platform_random.rb +0 -19
  118. data/lib/sass/util/ordered_hash.rb +0 -192
  119. data/test/sass/results/import_charset_1_8.css +0 -5
  120. data/test/sass/templates/import_charset_1_8.sass +0 -6
  121. data/vendor/listen/CHANGELOG.md +0 -1
  122. data/vendor/listen/CONTRIBUTING.md +0 -38
  123. data/vendor/listen/Gemfile +0 -20
  124. data/vendor/listen/Guardfile +0 -8
  125. data/vendor/listen/LICENSE +0 -20
  126. data/vendor/listen/README.md +0 -349
  127. data/vendor/listen/Rakefile +0 -5
  128. data/vendor/listen/Vagrantfile +0 -96
  129. data/vendor/listen/lib/listen.rb +0 -54
  130. data/vendor/listen/lib/listen/adapter.rb +0 -327
  131. data/vendor/listen/lib/listen/adapters/bsd.rb +0 -75
  132. data/vendor/listen/lib/listen/adapters/darwin.rb +0 -48
  133. data/vendor/listen/lib/listen/adapters/linux.rb +0 -81
  134. data/vendor/listen/lib/listen/adapters/polling.rb +0 -58
  135. data/vendor/listen/lib/listen/adapters/windows.rb +0 -91
  136. data/vendor/listen/lib/listen/directory_record.rb +0 -406
  137. data/vendor/listen/lib/listen/listener.rb +0 -323
  138. data/vendor/listen/lib/listen/turnstile.rb +0 -32
  139. data/vendor/listen/lib/listen/version.rb +0 -3
  140. data/vendor/listen/listen.gemspec +0 -28
  141. data/vendor/listen/spec/listen/adapter_spec.rb +0 -149
  142. data/vendor/listen/spec/listen/adapters/bsd_spec.rb +0 -36
  143. data/vendor/listen/spec/listen/adapters/darwin_spec.rb +0 -37
  144. data/vendor/listen/spec/listen/adapters/linux_spec.rb +0 -47
  145. data/vendor/listen/spec/listen/adapters/polling_spec.rb +0 -68
  146. data/vendor/listen/spec/listen/adapters/windows_spec.rb +0 -30
  147. data/vendor/listen/spec/listen/directory_record_spec.rb +0 -1250
  148. data/vendor/listen/spec/listen/listener_spec.rb +0 -258
  149. data/vendor/listen/spec/listen/turnstile_spec.rb +0 -56
  150. data/vendor/listen/spec/listen_spec.rb +0 -67
  151. data/vendor/listen/spec/spec_helper.rb +0 -25
  152. data/vendor/listen/spec/support/adapter_helper.rb +0 -666
  153. data/vendor/listen/spec/support/directory_record_helper.rb +0 -57
  154. data/vendor/listen/spec/support/fixtures_helper.rb +0 -29
  155. data/vendor/listen/spec/support/listeners_helper.rb +0 -179
  156. data/vendor/listen/spec/support/platform_helper.rb +0 -15
@@ -1,48 +0,0 @@
1
- module Listen
2
- module Adapters
3
-
4
- # Adapter implementation for Mac OS X `FSEvents`.
5
- #
6
- class Darwin < Adapter
7
- LAST_SEPARATOR_REGEX = /\/$/
8
-
9
-
10
- def self.target_os_regex; /darwin(1.+)?$/i; end
11
- def self.adapter_gem; 'rb-fsevent'; end
12
-
13
- private
14
-
15
- # Initializes a FSEvent worker and adds a watcher for
16
- # each directory passed to the adapter.
17
- #
18
- # @return [FSEvent] initialized worker
19
- #
20
- # @see Listen::Adapter#initialize_worker
21
- #
22
- def initialize_worker
23
- FSEvent.new.tap do |worker|
24
- worker.watch(directories.dup, :latency => latency) do |changes|
25
- next if paused
26
-
27
- mutex.synchronize do
28
- changes.each { |path| @changed_directories << path.sub(LAST_SEPARATOR_REGEX, '') }
29
- end
30
- end
31
- end
32
- end
33
-
34
- # Starts the worker in a new thread and sleep 0.1 second.
35
- #
36
- # @see Listen::Adapter#start_worker
37
- #
38
- def start_worker
39
- @worker_thread = Thread.new { worker.run }
40
- # The FSEvent worker needs some time to start up. Turnstiles can't
41
- # be used to wait for it as it runs in a loop.
42
- # TODO: Find a better way to block until the worker starts.
43
- sleep 0.1
44
- end
45
- end
46
-
47
- end
48
- end
@@ -1,81 +0,0 @@
1
- module Listen
2
- module Adapters
3
-
4
- # Listener implementation for Linux `inotify`.
5
- #
6
- class Linux < Adapter
7
- # Watched inotify events
8
- #
9
- # @see http://www.tin.org/bin/man.cgi?section=7&topic=inotify
10
- # @see https://github.com/nex3/rb-inotify/blob/master/lib/rb-inotify/notifier.rb#L99-L177
11
- #
12
- EVENTS = [:recursive, :attrib, :create, :delete, :move, :close_write]
13
-
14
- # The message to show when the limit of inotify watchers is not enough
15
- #
16
- INOTIFY_LIMIT_MESSAGE = <<-EOS.gsub(/^\s*/, '')
17
- Listen error: unable to monitor directories for changes.
18
-
19
- Please head to https://github.com/guard/listen/wiki/Increasing-the-amount-of-inotify-watchers
20
- for information on how to solve this issue.
21
- EOS
22
-
23
- def self.target_os_regex; /linux/i; end
24
- def self.adapter_gem; 'rb-inotify'; end
25
-
26
- # Initializes the Adapter.
27
- #
28
- # @see Listen::Adapter#initialize
29
- #
30
- def initialize(directories, options = {}, &callback)
31
- super
32
- rescue Errno::ENOSPC
33
- abort(INOTIFY_LIMIT_MESSAGE)
34
- end
35
-
36
- private
37
-
38
- # Initializes a INotify worker and adds a watcher for
39
- # each directory passed to the adapter.
40
- #
41
- # @return [INotify::Notifier] initialized worker
42
- #
43
- # @see Listen::Adapter#initialize_worker
44
- #
45
- def initialize_worker
46
- callback = lambda do |event|
47
- if paused || (
48
- # Event on root directory
49
- event.name == ""
50
- ) || (
51
- # INotify reports changes to files inside directories as events
52
- # on the directories themselves too.
53
- #
54
- # @see http://linux.die.net/man/7/inotify
55
- event.flags.include?(:isdir) and (event.flags & [:close, :modify]).any?
56
- )
57
- # Skip all of these!
58
- next
59
- end
60
-
61
- mutex.synchronize do
62
- @changed_directories << File.dirname(event.absolute_name)
63
- end
64
- end
65
-
66
- INotify::Notifier.new.tap do |worker|
67
- directories.each { |dir| worker.watch(dir, *EVENTS, &callback) }
68
- end
69
- end
70
-
71
- # Starts the worker in a new thread.
72
- #
73
- # @see Listen::Adapter#start_worker
74
- #
75
- def start_worker
76
- @worker_thread = Thread.new { worker.run }
77
- end
78
- end
79
-
80
- end
81
- end
@@ -1,58 +0,0 @@
1
- module Listen
2
- module Adapters
3
-
4
- DEFAULT_POLLING_LATENCY = 1.0
5
-
6
- # Polling Adapter that works cross-platform and
7
- # has no dependencies. This is the adapter that
8
- # uses the most CPU processing power and has higher
9
- # file IO than the other implementations.
10
- #
11
- class Polling < Adapter
12
- private
13
-
14
- # The default delay between checking for changes.
15
- #
16
- # @see Listen::Adapter#default_latency
17
- #
18
- def default_latency
19
- 1.0
20
- end
21
-
22
- # The thread on which the main thread should wait
23
- # when the adapter has been started in blocking mode.
24
- #
25
- # @see Listen::Adapter#blocking_thread
26
- #
27
- def blocking_thread
28
- poller_thread
29
- end
30
-
31
- # @see Listen::Adapter#start_worker
32
- #
33
- # @see Listen::Adapter#start_worker
34
- #
35
- def start_worker
36
- # The polling adapter has no worker! Sad panda! :'(
37
- end
38
-
39
- # Poll listener directory for file system changes.
40
- #
41
- # @see Listen::Adapter#poll_changed_directories
42
- #
43
- def poll_changed_directories
44
- until stopped
45
- next if paused
46
-
47
- start = Time.now.to_f
48
- callback.call(directories.dup, :recursive => true)
49
- turnstile.signal
50
- nap_time = latency - (Time.now.to_f - start)
51
- sleep(nap_time) if nap_time > 0
52
- end
53
- rescue Interrupt
54
- end
55
- end
56
-
57
- end
58
- end
@@ -1,91 +0,0 @@
1
- require 'set'
2
- require 'rubygems'
3
-
4
- module Listen
5
- module Adapters
6
-
7
- # Adapter implementation for Windows `wdm`.
8
- #
9
- class Windows < Adapter
10
-
11
- BUNDLER_DECLARE_GEM = <<-EOS.gsub(/^ {6}/, '')
12
- Please add the following to your Gemfile to avoid polling for changes:
13
- require 'rbconfig'
14
- gem 'wdm', '>= 0.1.0' if RbConfig::CONFIG['target_os'] =~ /mswin|mingw/i
15
- EOS
16
-
17
- def self.target_os_regex; /mswin|mingw/i; end
18
- def self.adapter_gem; 'wdm'; end
19
-
20
- # Checks if the adapter is usable on target OS.
21
- #
22
- # @return [Boolean] whether usable or not
23
- #
24
- def self.usable?
25
- super if mri? && at_least_ruby_1_9?
26
- end
27
-
28
- # Load the adapter gem
29
- #
30
- # @return [Boolean] whether required or not
31
- #
32
- def self.load_dependent_adapter
33
- super
34
- rescue Gem::LoadError
35
- Kernel.warn BUNDLER_DECLARE_GEM
36
- end
37
-
38
- private
39
-
40
- # Checks if Ruby engine is MRI.
41
- #
42
- # @return [Boolean]
43
- #
44
- def self.mri?
45
- defined?(RUBY_ENGINE) && RUBY_ENGINE == 'ruby'
46
- end
47
-
48
- # Checks if Ruby engine is MRI.
49
- #
50
- # @return [Boolean]
51
- #
52
- def self.at_least_ruby_1_9?
53
- Gem::Version.new(RUBY_VERSION.dup) >= Gem::Version.new('1.9.2')
54
- end
55
-
56
- # Initializes a WDM monitor and adds a watcher for
57
- # each directory passed to the adapter.
58
- #
59
- # @return [WDM::Monitor] initialized worker
60
- #
61
- # @see Listen::Adapter#initialize_worker
62
- #
63
- def initialize_worker
64
- callback = Proc.new do |change|
65
- next if paused
66
-
67
- mutex.synchronize do
68
- @changed_directories << File.dirname(change.path)
69
- end
70
- end
71
-
72
- WDM::Monitor.new.tap do |worker|
73
- directories.each { |dir| worker.watch_recursively(dir, &callback) }
74
- end
75
- end
76
-
77
- # Start the worker in a new thread and sleep 0.1 second.
78
- #
79
- # @see Listen::Adapter#start_worker
80
- #
81
- def start_worker
82
- @worker_thread = Thread.new { worker.run! }
83
- # Wait for the worker to start. This is needed to avoid a deadlock
84
- # when stopping immediately after starting.
85
- sleep 0.1
86
- end
87
-
88
- end
89
-
90
- end
91
- end
@@ -1,406 +0,0 @@
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
- # The default list of directories that get ignored by the listener.
15
- DEFAULT_IGNORED_DIRECTORIES = %w[.rbx .bundle .git .svn bundle log tmp vendor]
16
-
17
- # The default list of files that get ignored by the listener.
18
- DEFAULT_IGNORED_EXTENSIONS = %w[.DS_Store]
19
-
20
- # Defines the used precision based on the type of mtime returned by the
21
- # system (whether its in milliseconds or just seconds)
22
- #
23
- begin
24
- HIGH_PRECISION_SUPPORTED = File.mtime(__FILE__).to_f.to_s[-2..-1] != '.0'
25
- rescue
26
- HIGH_PRECISION_SUPPORTED = false
27
- end
28
-
29
- # Data structure used to save meta data about a path
30
- #
31
- MetaData = Struct.new(:type, :mtime)
32
-
33
- # Class methods
34
- #
35
- class << self
36
-
37
- # Creates the ignoring patterns from the default ignored
38
- # directories and extensions. It memoizes the generated patterns
39
- # to avoid unnecessary computation.
40
- #
41
- def generate_default_ignoring_patterns
42
- @@default_ignoring_patterns ||= Array.new.tap do |default_patterns|
43
- # Add directories
44
- ignored_directories = DEFAULT_IGNORED_DIRECTORIES.map { |d| Regexp.escape(d) }
45
- default_patterns << %r{^(?:#{ignored_directories.join('|')})/}
46
-
47
- # Add extensions
48
- ignored_extensions = DEFAULT_IGNORED_EXTENSIONS.map { |e| Regexp.escape(e) }
49
- default_patterns << %r{(?:#{ignored_extensions.join('|')})$}
50
- end
51
- end
52
- end
53
-
54
- # Initializes a directory record.
55
- #
56
- # @option [String] directory the directory to keep track of
57
- #
58
- def initialize(directory)
59
- raise ArgumentError, "The path '#{directory}' is not a directory!" unless File.directory?(directory)
60
-
61
- @directory, @sha1_checksums = File.expand_path(directory), Hash.new
62
- @ignoring_patterns, @filtering_patterns = Set.new, Set.new
63
-
64
- @ignoring_patterns.merge(DirectoryRecord.generate_default_ignoring_patterns)
65
- end
66
-
67
- # Returns the ignoring patterns in the record to know
68
- # which paths should be ignored.
69
- #
70
- # @return [Array<Regexp>] the ignoring patterns
71
- #
72
- def ignoring_patterns
73
- @ignoring_patterns.to_a
74
- end
75
-
76
- # Returns the filtering patterns in the record to know
77
- # which paths should be stored.
78
- #
79
- # @return [Array<Regexp>] the filtering patterns
80
- #
81
- def filtering_patterns
82
- @filtering_patterns.to_a
83
- end
84
-
85
- # Adds ignoring patterns to the record.
86
- #
87
- # @example Ignore some paths
88
- # ignore %r{^ignored/path/}, /man/
89
- #
90
- # @param [Regexp] regexps a list of patterns for ignoring paths
91
- #
92
- def ignore(*regexps)
93
- @ignoring_patterns.merge(regexps).reject! { |r| r.nil? }
94
- end
95
-
96
- # Replaces ignoring patterns in the record.
97
- #
98
- # @example Ignore only these paths
99
- # ignore! %r{^ignored/path/}, /man/
100
- #
101
- # @param [Regexp] regexps a list of patterns for ignoring paths
102
- #
103
- def ignore!(*regexps)
104
- @ignoring_patterns.replace(regexps).reject! { |r| r.nil? }
105
- end
106
-
107
- # Adds filtering patterns to the record.
108
- #
109
- # @example Filter some files
110
- # filter /\.txt$/, /.*\.zip/
111
- #
112
- # @param [Regexp] regexps a list of patterns for filtering files
113
- #
114
- def filter(*regexps)
115
- @filtering_patterns.merge(regexps).reject! { |r| r.nil? }
116
- end
117
-
118
- # Replaces filtering patterns in the record.
119
- #
120
- # @example Filter only these files
121
- # filter! /\.txt$/, /.*\.zip/
122
- #
123
- # @param [Regexp] regexps a list of patterns for filtering files
124
- #
125
- def filter!(*regexps)
126
- @filtering_patterns.replace(regexps).reject! { |r| r.nil? }
127
- end
128
-
129
- # Returns whether a path should be ignored or not.
130
- #
131
- # @param [String] path the path to test
132
- #
133
- # @return [Boolean]
134
- #
135
- def ignored?(path)
136
- path = relative_to_base(path)
137
- @ignoring_patterns.any? { |pattern| pattern =~ path }
138
- end
139
-
140
- # Returns whether a path should be filtered or not.
141
- #
142
- # @param [String] path the path to test
143
- #
144
- # @return [Boolean]
145
- #
146
- def filtered?(path)
147
- # When no filtering patterns are set, ALL files are stored.
148
- return true if @filtering_patterns.empty?
149
-
150
- path = relative_to_base(path)
151
- @filtering_patterns.any? { |pattern| pattern =~ path }
152
- end
153
-
154
- # Finds the paths that should be stored and adds them
155
- # to the paths' hash.
156
- #
157
- def build
158
- @paths = Hash.new { |h, k| h[k] = Hash.new }
159
- important_paths { |path| insert_path(path) }
160
- end
161
-
162
- # Detects changes in the passed directories, updates
163
- # the record with the new changes and returns the changes.
164
- #
165
- # @param [Array] directories the list of directories to scan for changes
166
- # @param [Hash] options
167
- # @option options [Boolean] recursive scan all sub-directories recursively
168
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
169
- #
170
- # @return [Hash<Array>] the changes
171
- #
172
- def fetch_changes(directories, options = {})
173
- @changes = { :modified => [], :added => [], :removed => [] }
174
- directories = directories.sort_by { |el| el.length }.reverse # diff sub-dir first
175
-
176
- directories.each do |directory|
177
- next unless directory[@directory] # Path is or inside directory
178
-
179
- detect_modifications_and_removals(directory, options)
180
- detect_additions(directory, options)
181
- end
182
-
183
- @changes
184
- end
185
-
186
- # Converts an absolute path to a path that's relative to the base directory.
187
- #
188
- # @param [String] path the path to convert
189
- #
190
- # @return [String] the relative path
191
- #
192
- def relative_to_base(path)
193
- path = path.dup
194
- regexp = "\\A#{Regexp.quote directory}(#{File::SEPARATOR}|\\z)"
195
- if path.respond_to?(:force_encoding)
196
- path.force_encoding("BINARY")
197
- regexp.force_encoding("BINARY")
198
- end
199
- if path.sub!(Regexp.new(regexp), '')
200
- path
201
- end
202
- end
203
-
204
- private
205
-
206
- # Detects modifications and removals recursively in a directory.
207
- #
208
- # @note Modifications detection begins by checking the modification time (mtime)
209
- # of files and then by checking content changes (using SHA1-checksum)
210
- # when the mtime of files is not changed.
211
- #
212
- # @param [String] directory the path to analyze
213
- # @param [Hash] options
214
- # @option options [Boolean] recursive scan all sub-directories recursively
215
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
216
- #
217
- def detect_modifications_and_removals(directory, options = {})
218
- paths[directory].each do |basename, meta_data|
219
- path = File.join(directory, basename)
220
- case meta_data.type
221
- when 'Dir'
222
- detect_modification_or_removal_for_dir(path, options)
223
- when 'File'
224
- detect_modification_or_removal_for_file(path, meta_data, options)
225
- end
226
- end
227
- end
228
-
229
- def detect_modification_or_removal_for_dir(path, options)
230
-
231
- # Directory still exists
232
- if File.directory?(path)
233
- detect_modifications_and_removals(path, options) if options[:recursive]
234
-
235
- # Directory has been removed
236
- else
237
- detect_modifications_and_removals(path, options)
238
- @paths[File.dirname(path)].delete(File.basename(path))
239
- @paths.delete("#{File.dirname(path)}/#{File.basename(path)}")
240
- end
241
- end
242
-
243
- def detect_modification_or_removal_for_file(path, meta_data, options)
244
- # File still exists
245
- if File.exist?(path)
246
- detect_modification(path, meta_data, options)
247
-
248
- # File has been removed
249
- else
250
- removal_detected(path, meta_data, options)
251
- end
252
- end
253
-
254
- def detect_modification(path, meta_data, options)
255
- new_mtime = mtime_of(path)
256
-
257
- # First check if we are in the same second (to update checksums)
258
- # before checking the time difference
259
- if (meta_data.mtime.to_i == new_mtime.to_i && content_modified?(path)) || meta_data.mtime < new_mtime
260
- modification_detected(path, meta_data, new_mtime, options)
261
- end
262
- end
263
-
264
- def modification_detected(path, meta_data, new_mtime, options)
265
- # Update the sha1 checksum of the file
266
- update_sha1_checksum(path)
267
-
268
- # Update the meta data of the file
269
- meta_data.mtime = new_mtime
270
- @paths[File.dirname(path)][File.basename(path)] = meta_data
271
-
272
- @changes[:modified] << (options[:relative_paths] ? relative_to_base(path) : path)
273
- end
274
-
275
- def removal_detected(path, meta_data, options)
276
- @paths[File.dirname(path)].delete(File.basename(path))
277
- @sha1_checksums.delete(path)
278
- @changes[:removed] << (options[:relative_paths] ? relative_to_base(path) : path)
279
- end
280
-
281
- # Detects additions in a directory.
282
- #
283
- # @param [String] directory the path to analyze
284
- # @param [Hash] options
285
- # @option options [Boolean] recursive scan all sub-directories recursively
286
- # @option options [Boolean] relative_paths whether or not to use relative paths for changes
287
- #
288
- def detect_additions(directory, options = {})
289
- # Don't process removed directories
290
- return unless File.exist?(directory)
291
-
292
- Find.find(directory) do |path|
293
- next if path == @directory
294
-
295
- if File.directory?(path)
296
- # Add a trailing slash to directories when checking if a directory is
297
- # ignored to optimize finding them as Find.find doesn't.
298
- if ignored?(path + File::SEPARATOR) || (directory != path && (!options[:recursive] && existing_path?(path)))
299
- Find.prune # Don't look any further into this directory.
300
- else
301
- insert_path(path)
302
- end
303
- elsif !ignored?(path) && filtered?(path) && !existing_path?(path)
304
- if File.file?(path)
305
- @changes[:added] << (options[:relative_paths] ? relative_to_base(path) : path)
306
- insert_path(path)
307
- end
308
- end
309
- end
310
- end
311
-
312
- # Returns whether or not a file's content has been modified by
313
- # comparing the SHA1-checksum to a stored one.
314
- # Ensure that the SHA1-checksum is inserted to the sha1_checksums
315
- # array for later comparaison if false.
316
- #
317
- # @param [String] path the file path
318
- #
319
- def content_modified?(path)
320
- return false unless File.ftype(path) == 'file'
321
- @sha1_checksum = sha1_checksum(path)
322
- if sha1_checksums[path] == @sha1_checksum || !sha1_checksums.key?(path)
323
- update_sha1_checksum(path)
324
- false
325
- else
326
- true
327
- end
328
- end
329
-
330
- # Inserts a SHA1-checksum path in @SHA1-checksums hash.
331
- #
332
- # @param [String] path the SHA1-checksum path to insert in @sha1_checksums.
333
- #
334
- def update_sha1_checksum(path)
335
- if @sha1_checksum ||= sha1_checksum(path)
336
- @sha1_checksums[path] = @sha1_checksum
337
- @sha1_checksum = nil
338
- end
339
- end
340
-
341
- # Returns the SHA1-checksum for the file path.
342
- #
343
- # @param [String] path the file path
344
- #
345
- def sha1_checksum(path)
346
- Digest::SHA1.file(path).to_s
347
- rescue
348
- nil
349
- end
350
-
351
- # Traverses the base directory looking for paths that should
352
- # be stored; thus paths that are filtered or not ignored.
353
- #
354
- # @yield [path] an important path
355
- #
356
- def important_paths
357
- Find.find(directory) do |path|
358
- next if path == directory
359
-
360
- if File.directory?(path)
361
- # Add a trailing slash to directories when checking if a directory is
362
- # ignored to optimize finding them as Find.find doesn't.
363
- if ignored?(path + File::SEPARATOR)
364
- Find.prune # Don't look any further into this directory.
365
- else
366
- yield(path)
367
- end
368
- elsif !ignored?(path) && filtered?(path)
369
- yield(path)
370
- end
371
- end
372
- end
373
-
374
- # Inserts a path with its type (Dir or File) in paths hash.
375
- #
376
- # @param [String] path the path to insert in @paths.
377
- #
378
- def insert_path(path)
379
- meta_data = MetaData.new
380
- meta_data.type = File.directory?(path) ? 'Dir' : 'File'
381
- meta_data.mtime = mtime_of(path) unless meta_data.type == 'Dir' # mtimes of dirs are not used yet
382
- @paths[File.dirname(path)][File.basename(path)] = meta_data
383
- rescue Errno::ENOENT
384
- end
385
-
386
- # Returns whether or not a path exists in the paths hash.
387
- #
388
- # @param [String] path the path to check
389
- #
390
- # @return [Boolean]
391
- #
392
- def existing_path?(path)
393
- paths[File.dirname(path)][File.basename(path)] != nil
394
- end
395
-
396
- # Returns the modification time of a file based on the precision defined by the system
397
- #
398
- # @param [String] file the file for which the mtime must be returned
399
- #
400
- # @return [Fixnum, Float] the mtime of the file
401
- #
402
- def mtime_of(file)
403
- File.lstat(file).mtime.send(HIGH_PRECISION_SUPPORTED ? :to_f : :to_i)
404
- end
405
- end
406
- end