From a46993c823ff2ec1c5fd13f49168d5428e5759da Mon Sep 17 00:00:00 2001 From: MoHKale Date: Mon, 30 Dec 2019 21:01:08 +0000 Subject: [PATCH 1/3] guarantee jekyll-watch respects users exclude configuration by double-tapping (sly zombieland reference :laughing:) paths which don't match to exact (existing) files. this patch stores any of these non-absolute paths as wildcard paths; which it then rechecks & then strips from paths in the response handler for the listen package. NOTE maybe it'd be better & more reliable to simply use fnmatch? exclusively and not pass any filtering regexps to listen. WARN I've also modified the config-file check to only take place when your src directory is same as the project root directory, because the config file exists at the project root and if your src is in some subdirectory, then it's guaranteed to not be found by the listener package. I haven't tested this yet because I'll need to create a new empty jekyll site, but I have no doubt it should work. --- lib/jekyll/watcher.rb | 89 +++++++++++++++++++++++++++++-------------- 1 file changed, 61 insertions(+), 28 deletions(-) diff --git a/lib/jekyll/watcher.rb b/lib/jekyll/watcher.rb index ea49ad6..3c2c114 100644 --- a/lib/jekyll/watcher.rb +++ b/lib/jekyll/watcher.rb @@ -43,25 +43,33 @@ def watch(options, site = nil) private def build_listener(site, options) + ignore_regexps, ignore_paths = listen_ignore_paths(options) + Listen.to( options["source"], - :ignore => listen_ignore_paths(options), + :ignore => ignore_regexps, :force_polling => options["force_polling"], - &listen_handler(site) + &listen_handler(site, ignore_paths) ) end - def listen_handler(site) + def listen_handler(site, ignore_paths) proc do |modified, added, removed| - t = Time.now + modified = strip_ignore_paths(modified, ignore_paths) + added = strip_ignore_paths(added, ignore_paths) + removed = strip_ignore_paths(removed, ignore_paths) + c = modified + added + removed - n = c.length + unless c.empty? + t = Time.now + n = c.length - Jekyll.logger.info "Regenerating:", - "#{n} file(s) changed at #{t.strftime("%Y-%m-%d %H:%M:%S")}" + Jekyll.logger.info "Regenerating:", + "#{n} file(s) changed at #{t.strftime("%Y-%m-%d %H:%M:%S")}" - c.each { |path| Jekyll.logger.info "", path["#{site.source}/".length..-1] } - process(site, t) + c.each { |path| Jekyll.logger.info "", path["#{site.source}/".length..-1] } + process(site, t) + end end end @@ -74,13 +82,18 @@ def normalize_encoding(obj, desired_encoding) end end + # paths that user specified to be excluded in their _config.yml file. def custom_excludes(options) - Array(options["exclude"]).map { |e| Jekyll.sanitized_path(options["source"], e) } + options.fetch("exclude", []).map { |e| Jekyll.sanitized_path(options["source"], e) } end def config_files(options) - %w(yml yaml toml).map do |ext| - Jekyll.sanitized_path(options["source"], "_config.#{ext}") + # only check to exclude config file when config file is within our src directory + # which can only happen if our src directory is the same as our cwd. + if Pathname.new(options["source"]).expand_path.eql?(Pathname.new(".").expand_path) + %w(yml yaml toml).map(&"^_config\.".method(:+)) + else + [] end end @@ -89,34 +102,54 @@ def to_exclude(options) config_files(options), options["destination"], custom_excludes(options), - ].flatten + ].flatten.map { |e| normalize_encoding(e, options["source"].encoding) } end # Paths to ignore for the watch option # # options - A Hash of options passed to the command # - # Returns a list of relative paths from source that should be ignored + # Returns a tuple where the first entry is a list of regular + # expressions relating to exact (existing) files to + # ignore and the second is a list of fnmatch patterns + # to ignore. def listen_ignore_paths(options) source = Pathname.new(options["source"]).expand_path - paths = to_exclude(options) - - paths.map do |p| - absolute_path = Pathname.new(normalize_encoding(p, options["source"].encoding)).expand_path - next unless absolute_path.exist? - - begin - relative_path = absolute_path.relative_path_from(source).to_s + exclusion_fnmatch_paths = Array.new() + + exclusion_regexps = to_exclude(options).map do |path| + # convert to absolute path from the source directory + absolute_path = Pathname.new(path).expand_path + relative_path = absolute_path.relative_path_from(source) + + unless absolute_path.exist? + # maybe wildcard, or just a file that doesn't exist. + exclusion_fnmatch_paths << relative_path.to_s + nil + else relative_path = File.join(relative_path, "") if absolute_path.directory? - unless relative_path.start_with?("../") - path_to_ignore = %r!^#{Regexp.escape(relative_path)}! - Jekyll.logger.debug "Watcher:", "Ignoring #{path_to_ignore}" - path_to_ignore + + begin + unless relative_path.start_with?("../") + Regexp.new(Regexp.escape(relative_path)).tap do |pattern| + Jekyll.logger.debug "Watcher:", "Ignoring #{pattern}" + end + end + rescue ArgumentError + # Could not find a relative path end - rescue ArgumentError - # Could not find a relative path end end.compact + [%r!^\.jekyll\-metadata!] + + [exclusion_regexps, exclusion_fnmatch_paths] + end + + # remove any paths from PATHS which're matched to by some fnmatch + # pattern in IGNORE_PATTERNS + def strip_ignore_paths(paths, ignore_patterns) + paths.select do |path| + ignore_patterns.find { |pattern| File.fnmatch?(pattern, path) }.nil? + end end def sleep_forever From 45f1dfe098d8599977f68bdbca759d78dd5c9770 Mon Sep 17 00:00:00 2001 From: MoHKale Date: Mon, 30 Dec 2019 21:42:04 +0000 Subject: [PATCH 2/3] fix broken test cases and revert _config.yml exclusion check I couldn't figure out how to make rspec change dir to the root of the site, so I couldn't make it pass the test. Just ended up reverting to the original method name :(. Also didn't realise the regexps used by listen_ignore_paths began with a ^, my bad. Fixed now. --- lib/jekyll/watcher.rb | 19 +++++++------------ spec/watcher_spec.rb | 3 ++- 2 files changed, 9 insertions(+), 13 deletions(-) diff --git a/lib/jekyll/watcher.rb b/lib/jekyll/watcher.rb index 3c2c114..370d142 100644 --- a/lib/jekyll/watcher.rb +++ b/lib/jekyll/watcher.rb @@ -88,12 +88,8 @@ def custom_excludes(options) end def config_files(options) - # only check to exclude config file when config file is within our src directory - # which can only happen if our src directory is the same as our cwd. - if Pathname.new(options["source"]).expand_path.eql?(Pathname.new(".").expand_path) - %w(yml yaml toml).map(&"^_config\.".method(:+)) - else - [] + %w(yml yaml toml).map do |ext| + Jekyll.sanitized_path(options["source"], "_config.#{ext}") end end @@ -115,23 +111,22 @@ def to_exclude(options) # to ignore. def listen_ignore_paths(options) source = Pathname.new(options["source"]).expand_path - exclusion_fnmatch_paths = Array.new() + exclusion_fnmatch_paths = [] exclusion_regexps = to_exclude(options).map do |path| # convert to absolute path from the source directory absolute_path = Pathname.new(path).expand_path - relative_path = absolute_path.relative_path_from(source) + relative_path = absolute_path.relative_path_from(source).to_s - unless absolute_path.exist? + if !absolute_path.exist? # maybe wildcard, or just a file that doesn't exist. - exclusion_fnmatch_paths << relative_path.to_s - nil + nil.tap { exclusion_fnmatch_paths << relative_path } else relative_path = File.join(relative_path, "") if absolute_path.directory? begin unless relative_path.start_with?("../") - Regexp.new(Regexp.escape(relative_path)).tap do |pattern| + %r!^#{Regexp.escape(relative_path)}!.tap do |pattern| Jekyll.logger.debug "Watcher:", "Ignoring #{pattern}" end end diff --git a/spec/watcher_spec.rb b/spec/watcher_spec.rb index 98af237..76b6a57 100644 --- a/spec/watcher_spec.rb +++ b/spec/watcher_spec.rb @@ -1,3 +1,4 @@ +# coding: utf-8 require "spec_helper" describe(Jekyll::Watcher) do @@ -97,7 +98,7 @@ end context "#listen_ignore_paths" do - let(:ignored) { subject.send(:listen_ignore_paths, options) } + let(:ignored) { subject.send(:listen_ignore_paths, options)[0] } let(:metadata_path) { Jekyll.sanitized_path(options["source"], ".jekyll-metadata") } before(:each) { FileUtils.touch(metadata_path) } From 93c5ae3004fe13b5a89a5d146eef261347312ce4 Mon Sep 17 00:00:00 2001 From: MoHKale Date: Tue, 31 Dec 2019 13:47:02 +0000 Subject: [PATCH 3/3] refactor "guarantee jekyll-watch respects users exclude configuration" see https://github.com/jekyll/jekyll-watch/pull/92 --- lib/jekyll/watcher.rb | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/lib/jekyll/watcher.rb b/lib/jekyll/watcher.rb index 370d142..5e8d50b 100644 --- a/lib/jekyll/watcher.rb +++ b/lib/jekyll/watcher.rb @@ -60,6 +60,7 @@ def listen_handler(site, ignore_paths) removed = strip_ignore_paths(removed, ignore_paths) c = modified + added + removed + unless c.empty? t = Time.now n = c.length @@ -83,8 +84,11 @@ def normalize_encoding(obj, desired_encoding) end # paths that user specified to be excluded in their _config.yml file. + # could be nil, a single string or an array of strings. + # + # Returns an array of paths joined to the src directory. def custom_excludes(options) - options.fetch("exclude", []).map { |e| Jekyll.sanitized_path(options["source"], e) } + Array(options["exclude"]).map { |e| Jekyll.sanitized_path(options["source"], e) } end def config_files(options) @@ -98,7 +102,7 @@ def to_exclude(options) config_files(options), options["destination"], custom_excludes(options), - ].flatten.map { |e| normalize_encoding(e, options["source"].encoding) } + ].flat_map { |e| normalize_encoding(e, options["source"].encoding) } end # Paths to ignore for the watch option @@ -118,21 +122,21 @@ def listen_ignore_paths(options) absolute_path = Pathname.new(path).expand_path relative_path = absolute_path.relative_path_from(source).to_s - if !absolute_path.exist? - # maybe wildcard, or just a file that doesn't exist. - nil.tap { exclusion_fnmatch_paths << relative_path } - else + if absolute_path.exist? relative_path = File.join(relative_path, "") if absolute_path.directory? begin - unless relative_path.start_with?("../") - %r!^#{Regexp.escape(relative_path)}!.tap do |pattern| - Jekyll.logger.debug "Watcher:", "Ignoring #{pattern}" - end + next if relative_path.start_with?("../") + + %r!^#{Regexp.escape(relative_path)}!.tap do |pattern| + Jekyll.logger.debug "Watcher:", "Ignoring #{pattern}" end rescue ArgumentError # Could not find a relative path end + else + # maybe wildcard, or just a file that doesn't exist. + nil.tap { exclusion_fnmatch_paths << relative_path } end end.compact + [%r!^\.jekyll\-metadata!] @@ -143,6 +147,7 @@ def listen_ignore_paths(options) # pattern in IGNORE_PATTERNS def strip_ignore_paths(paths, ignore_patterns) paths.select do |path| + # true when every patten in ignore_patterns does not match to path. ignore_patterns.find { |pattern| File.fnmatch?(pattern, path) }.nil? end end