nanoc 3.6.2 → 3.6.3

Sign up to get free protection for your applications and to get access to all the features.
@@ -33,6 +33,18 @@ module Nanoc::CLI::Commands
33
33
  # @abstract Subclasses must override {#start} and may override {#stop}.
34
34
  class Listener
35
35
 
36
+ def initialize(params={})
37
+ end
38
+
39
+ # @param [Nanoc::CLI::CommandRunner] command_runner The command runner for this listener
40
+ #
41
+ # @return [Boolean] true if this listener should be enabled for the given command runner, false otherwise
42
+ #
43
+ # @abstract Returns `true` by default, but subclasses may override this.
44
+ def self.enable_for?(command_runner)
45
+ true
46
+ end
47
+
36
48
  # Starts the listener. Subclasses should override this method and set up listener notifications.
37
49
  #
38
50
  # @return [void]
@@ -53,6 +65,11 @@ module Nanoc::CLI::Commands
53
65
  # Generates diffs for every output file written
54
66
  class DiffGenerator < Listener
55
67
 
68
+ # @see Listener#enable_for?
69
+ def self.enable_for?(command_runner)
70
+ command_runner.site.config[:enable_output_diff]
71
+ end
72
+
56
73
  # @see Listener#start
57
74
  def start
58
75
  require 'tempfile'
@@ -138,6 +155,11 @@ module Nanoc::CLI::Commands
138
155
  # Records the time spent per filter and per item representation
139
156
  class TimingRecorder < Listener
140
157
 
158
+ # @see Listener#enable_for?
159
+ def self.enable_for?(command_runner)
160
+ command_runner.options.fetch(:verbose, false)
161
+ end
162
+
141
163
  # @option params [Array<Nanoc::ItemRep>] :reps The list of item representations in the site
142
164
  def initialize(params={})
143
165
  @filter_times = {}
@@ -205,7 +227,7 @@ module Nanoc::CLI::Commands
205
227
  tot = format('%5.2f', tot)
206
228
 
207
229
  # Output stats
208
- filter_name = format("%#{max_filter_name_length}s", filter_name)
230
+ filter_name = format("%#{max}s", filter_name)
209
231
  puts "#{filter_name} | #{count} #{min}s #{avg}s #{max}s #{tot}s"
210
232
  end
211
233
 
@@ -214,7 +236,12 @@ module Nanoc::CLI::Commands
214
236
  # Controls garbage collection so that it only occurs once every 20 items
215
237
  class GCController < Listener
216
238
 
217
- def initialize
239
+ # @see Listener#enable_for?
240
+ def self.enable_for?(command_runner)
241
+ ! ENV.has_key?('TRAVIS')
242
+ end
243
+
244
+ def initialize(params={})
218
245
  @gc_count = 0
219
246
  end
220
247
 
@@ -241,6 +268,11 @@ module Nanoc::CLI::Commands
241
268
  # Prints debug information (compilation started/ended, filtering started/ended, …)
242
269
  class DebugPrinter < Listener
243
270
 
271
+ # @see Listener#enable_for?
272
+ def self.enable_for?(command_runner)
273
+ command_runner.debug?
274
+ end
275
+
244
276
  # @see Listener#start
245
277
  def start
246
278
  Nanoc::NotificationCenter.on(:compilation_started) do |rep|
@@ -315,15 +347,23 @@ module Nanoc::CLI::Commands
315
347
 
316
348
  end
317
349
 
350
+ def initialize(options, arguments, command, params={})
351
+ super(options, arguments, command)
352
+ @listener_classes = params.fetch(:listener_classes, self.default_listener_classes)
353
+ end
354
+
318
355
  def run
356
+ time_before = Time.now
357
+
319
358
  self.load_site
320
359
  self.check_for_deprecated_usage
321
- self.setup_listeners
322
360
 
323
361
  puts "Compiling site…"
324
- time_before = Time.now
325
- self.site.compile
326
- self.prune
362
+ self.run_listeners_while do
363
+ self.site.compile
364
+ self.prune
365
+ end
366
+
327
367
  time_after = Time.now
328
368
  puts
329
369
  puts "Site compiled in #{format('%.2f', time_after - time_before)}s."
@@ -337,28 +377,33 @@ module Nanoc::CLI::Commands
337
377
  end
338
378
  end
339
379
 
340
- def setup_listeners
341
- @listeners = []
342
-
343
- if self.site.config[:enable_output_diff]
344
- @listeners << Nanoc::CLI::Commands::Compile::DiffGenerator.new
345
- end
346
-
347
- if self.debug?
348
- @listeners << Nanoc::CLI::Commands::Compile::DebugPrinter.new
349
- end
380
+ def default_listener_classes
381
+ [
382
+ Nanoc::CLI::Commands::Compile::DiffGenerator,
383
+ Nanoc::CLI::Commands::Compile::DebugPrinter,
384
+ Nanoc::CLI::Commands::Compile::TimingRecorder,
385
+ Nanoc::CLI::Commands::Compile::GCController,
386
+ Nanoc::CLI::Commands::Compile::FileActionPrinter
387
+ ]
388
+ end
350
389
 
351
- if options.fetch(:verbose, false)
352
- @listeners << Nanoc::CLI::Commands::Compile::TimingRecorder.new(:reps => self.reps)
353
- end
390
+ def setup_listeners
391
+ @listeners = @listener_classes.
392
+ select { |klass| klass.enable_for?(self) }.
393
+ map { |klass| klass.new(:reps => self.reps) }
354
394
 
355
- unless ENV.has_key?('TRAVIS')
356
- @listeners << Nanoc::CLI::Commands::Compile::GCController.new
357
- end
395
+ @listeners.each { |s| s.start }
396
+ end
358
397
 
359
- @listeners << Nanoc::CLI::Commands::Compile::FileActionPrinter.new(:reps => self.reps)
398
+ def listeners
399
+ @listeners
400
+ end
360
401
 
361
- @listeners.each { |s| s.start }
402
+ def run_listeners_while
403
+ self.setup_listeners
404
+ yield
405
+ ensure
406
+ self.teardown_listeners
362
407
  end
363
408
 
364
409
  def teardown_listeners
@@ -80,12 +80,12 @@ module Nanoc::CLI::Commands
80
80
  rebuilder.call(removed[0]) if removed[0]
81
81
  end
82
82
 
83
- listener = Listen::MultiListener.new(*dirs_to_watch).change(&callback)
84
- listener_root = Listen::MultiListener.new('', :filter => files_to_watch, :ignore => ignore_dir).change(&callback)
83
+ listener = Listen::Listener.new(*dirs_to_watch).change(&callback)
84
+ listener_root = Listen::Listener.new('.', :filter => files_to_watch, :ignore => ignore_dir).change(&callback)
85
85
 
86
86
  begin
87
- listener_root.start(false)
88
- listener.start
87
+ listener_root.start
88
+ listener.start!
89
89
  rescue Interrupt
90
90
  listener.stop
91
91
  listener_root.stop
@@ -98,30 +98,47 @@ module Nanoc::CLI::Commands
98
98
  # A list of commandline tool names that can be used to send notifications
99
99
  TOOLS = %w( growlnotify notify-send ) unless defined? TOOLS
100
100
 
101
- # The tool to use for discovering binaries' locations
102
- FIND_BINARY_COMMAND = RUBY_PLATFORM =~ /mingw|mswin/ ? "where" : "which" unless defined? FIND_BINARY_COMMAND
103
-
104
101
  # Send a notification. If no notifier is found, no notification will be
105
102
  # created.
106
103
  #
107
104
  # @param [String] message The message to include in the notification
108
105
  def notify(message)
109
106
  return if tool.nil?
110
- send(tool.tr('-', '_'), message)
107
+ if tool == 'growlnotify' && self.on_windows?
108
+ self.growlnotify_windows(message)
109
+ else
110
+ send(tool.tr('-', '_'), message)
111
+ end
112
+ end
113
+
114
+ protected
115
+
116
+ def have_tool_nix?(tool)
117
+ !`which #{tool}`.empty?
118
+ rescue Errno::ENOENT
119
+ false
120
+ end
121
+
122
+ def have_tool_windows?(tool)
123
+ !`where #{tool} 2> nul`.empty?
124
+ rescue Errno::ENOENT
125
+ false
111
126
  end
112
127
 
113
- private
128
+ def have_tool?(tool)
129
+ if self.on_windows?
130
+ self.have_tool_windows?(tool)
131
+ else
132
+ self.have_tool_nix?(tool)
133
+ end
134
+ end
114
135
 
115
136
  def tool
116
137
  @tool ||= begin
117
138
  require 'terminal-notifier'
118
139
  'terminal-notify'
119
140
  rescue LoadError
120
- begin
121
- TOOLS.find { |t| !`#{FIND_BINARY_COMMAND} #{t}`.empty? }
122
- rescue Errno::ENOENT
123
- nil
124
- end
141
+ TOOLS.find { |t| have_tool?(t) }
125
142
  end
126
143
  end
127
144
 
@@ -129,14 +146,30 @@ module Nanoc::CLI::Commands
129
146
  TerminalNotifier.notify(message, :title => "nanoc")
130
147
  end
131
148
 
149
+ def growlnotify_cmd_for(message)
150
+ [ 'growlnotify', '-m', message ]
151
+ end
152
+
132
153
  def growlnotify(message)
133
- system('growlnotify', '-m', message)
154
+ system(*self.growlnotify_cmd_for(message))
155
+ end
156
+
157
+ def growlnotify_windows_cmd_for(message)
158
+ [ 'growlnotify', '/t:nanoc', message ]
159
+ end
160
+
161
+ def growlnotify_windows(message)
162
+ system(*self.growlnotify_windows_cmd_for(message))
134
163
  end
135
164
 
136
165
  def notify_send(message)
137
166
  system('notify-send', message)
138
167
  end
139
168
 
169
+ def on_windows?
170
+ !!(RUBY_PLATFORM =~ /(mingw|bccwin|wince|mswin32)/i)
171
+ end
172
+
140
173
  end
141
174
 
142
175
  end
@@ -52,7 +52,7 @@ module Nanoc::DataSources
52
52
  create_object('layouts', content, attributes, identifier, params)
53
53
  end
54
54
 
55
- private
55
+ protected
56
56
 
57
57
  # Creates a new object (item or layout) on disk in dir_name according to
58
58
  # the given identifier. The file will have its attributes taken from the
@@ -144,7 +144,7 @@ module Nanoc::DataSources
144
144
  # }
145
145
  def all_split_files_in(dir_name)
146
146
  # Get all good file names
147
- filenames = Dir[dir_name + '/**/*'].select { |i| File.file?(i) }
147
+ filenames = self.all_files_in(dir_name)
148
148
  filenames.reject! { |fn| fn =~ /(~|\.orig|\.rej|\.bak)$/ }
149
149
 
150
150
  # Group by identifier
@@ -173,6 +173,11 @@ module Nanoc::DataSources
173
173
  grouped_filenames
174
174
  end
175
175
 
176
+ # Returns all files in the given directory and directories below it.
177
+ def all_files_in(dir_name)
178
+ Nanoc::Extra::FilesystemTools.all_files_in(dir_name)
179
+ end
180
+
176
181
  # Returns the filename for the given base filename and the extension.
177
182
  #
178
183
  # If the extension is nil, this function should return nil as well.
@@ -26,24 +26,21 @@ module Nanoc::DataSources
26
26
  # exclude them from the Blogging helper's atom feed generator, among other
27
27
  # things.
28
28
  class Static < Nanoc::DataSource
29
+
29
30
  identifier :static
30
31
 
31
32
  def items
32
33
  # Get prefix
33
34
  prefix = config[:prefix] || 'static'
34
35
 
35
- # Get all files under prefix dir
36
- filenames = Dir[prefix + '/**/*'].select { |f| File.file?(f) }
37
-
38
36
  # Convert filenames to items
39
- filenames.map do |filename|
37
+ self.all_files_in(prefix).map do |filename|
40
38
  attributes = {
41
39
  :extension => File.extname(filename)[1..-1],
42
40
  :filename => filename,
43
41
  }
44
42
  attributes[:is_hidden] = true unless config[:hide_items] == false
45
43
  identifier = filename[(prefix.length+1)..-1] + '/'
46
-
47
44
  mtime = File.mtime(filename)
48
45
  checksum = Pathname.new(filename).checksum
49
46
 
@@ -56,5 +53,12 @@ module Nanoc::DataSources
56
53
  end
57
54
  end
58
55
 
56
+ protected
57
+
58
+ def all_files_in(dir_name)
59
+ Nanoc::Extra::FilesystemTools.all_files_in(dir_name)
60
+ end
61
+
59
62
  end
63
+
60
64
  end
@@ -5,6 +5,7 @@ module Nanoc::Extra
5
5
  autoload 'AutoCompiler', 'nanoc/extra/auto_compiler'
6
6
  autoload 'Checking', 'nanoc/extra/checking'
7
7
  autoload 'CHiCk', 'nanoc/extra/chick'
8
+ autoload 'FilesystemTools', 'nanoc/extra/filesystem_tools'
8
9
  autoload 'LinkCollector', 'nanoc/extra/link_collector.rb'
9
10
  autoload 'Pruner', 'nanoc/extra/pruner'
10
11
  autoload 'Validators', 'nanoc/extra/validators'
@@ -136,9 +136,22 @@ module ::Nanoc::Extra::Checking::Checks
136
136
  raise 'should not have gotten here'
137
137
  end
138
138
 
139
+ def path_for_url(url)
140
+ if url.path.nil? || url.path.empty?
141
+ path = '/'
142
+ else
143
+ path = url.path
144
+ end
145
+
146
+ if url.query
147
+ path << '?' << url.query
148
+ end
149
+
150
+ path
151
+ end
152
+
139
153
  def request_url_once(url, req_method = Net::HTTP::Head)
140
- path = (url.path.nil? || url.path.empty? ? '/' : url.path)
141
- req = req_method.new(path)
154
+ req = req_method.new(self.path_for_url(url))
142
155
  http = Net::HTTP.new(url.host, url.port)
143
156
  if url.instance_of? URI::HTTPS
144
157
  http.use_ssl = true
@@ -16,7 +16,7 @@ module Nanoc::Extra
16
16
  def freeze
17
17
  end
18
18
 
19
- def respond_to?(meth)
19
+ def respond_to?(meth, include_all=false)
20
20
  file_instance_methods.include?(meth.to_sym)
21
21
  end
22
22
 
@@ -0,0 +1,124 @@
1
+ # encoding: utf-8
2
+
3
+ module Nanoc::Extra
4
+
5
+ # Contains useful functions for managing the filesystem.
6
+ #
7
+ # @api private
8
+ module FilesystemTools
9
+
10
+ # Error that is raised when too many symlink indirections are encountered.
11
+ #
12
+ # @api private
13
+ class MaxSymlinkDepthExceededError < ::Nanoc::Errors::GenericTrivial
14
+
15
+ # @return [String] The last filename that was attempted to be
16
+ # resolved before giving up
17
+ attr_reader :filename
18
+
19
+ # @param [String] filename The last filename that was attempted to be
20
+ # resolved before giving up
21
+ def initialize(filename)
22
+ @filename = filename
23
+ super("Too many indirections while resolving symlinks. I gave up after finding out #{filename} was yet another symlink. Sorry!")
24
+ end
25
+
26
+ end
27
+
28
+ # Error that is raised when a file of an unknown type is encountered
29
+ # (something other than file, directory or link).
30
+ #
31
+ # @api private
32
+ class UnsupportedFileTypeError < ::Nanoc::Errors::GenericTrivial
33
+
34
+ # @return [String] The filename of the file whose type is not supported
35
+ attr_reader :filename
36
+
37
+ # @param [String] filename The filename of the file whose type is not
38
+ # supported
39
+ def initialize(filename)
40
+ @filename = filename
41
+ super("The file at #{filename} is of an unsupported type (expected file, directory or link, but it is #{File.ftype(filename)}")
42
+ end
43
+
44
+ end
45
+
46
+ # Returns all files in the given directory and directories below it,
47
+ # following symlinks up to a maximum of `recursion_limit` times.
48
+ #
49
+ # @param [String] dir_name The name of the directory whose contents to
50
+ # fetch
51
+ #
52
+ # @param [Integer] recursion_limit The maximum number of times to
53
+ # recurse into a symlink to a directory
54
+ #
55
+ # @return [Array<String>] A list of filenames
56
+ #
57
+ # @raise [MaxSymlinkDepthExceededError] if too many indirections are
58
+ # encountered while resolving symlinks
59
+ #
60
+ # @raise [UnsupportedFileTypeError] if a file of an unsupported type is
61
+ # detected (something other than file, directory or link)
62
+ def all_files_in(dir_name, recursion_limit=10)
63
+ Dir[dir_name + '/**/*'].map do |fn|
64
+ case File.ftype(fn)
65
+ when 'link'
66
+ if 0 == recursion_limit
67
+ raise MaxSymlinkDepthExceededError.new(fn)
68
+ else
69
+ absolute_target = self.resolve_symlink(fn)
70
+ if File.file?(absolute_target)
71
+ fn
72
+ else
73
+ all_files_in(absolute_target, recursion_limit-1).map do |sfn|
74
+ fn + sfn[absolute_target.size..-1]
75
+ end
76
+ end
77
+ end
78
+ when 'file'
79
+ fn
80
+ when 'directory'
81
+ nil
82
+ else
83
+ raise UnsupportedFileTypeError.new(fn)
84
+ end
85
+ end.compact.flatten
86
+ end
87
+ module_function :all_files_in
88
+
89
+ # Resolves the given symlink into an absolute path.
90
+ #
91
+ # @param [String] filename The filename of the symlink to resolve
92
+ #
93
+ # @param [Integer] recursion_limit The maximum number of times to recurse
94
+ # into a symlink
95
+ #
96
+ # @return [String] The absolute resolved filename of the symlink target
97
+ #
98
+ # @raise [MaxSymlinkDepthExceededError] if too many indirections are
99
+ # encountered while resolving symlinks
100
+ #
101
+ # @raise [UnsupportedFileTypeError] if a file of an unsupported type is
102
+ # detected (something other than file, directory or link)
103
+ def resolve_symlink(filename, recursion_limit=5)
104
+ target = File.readlink(filename)
105
+ absolute_target = File.expand_path(target, File.dirname(filename))
106
+
107
+ case File.ftype(absolute_target)
108
+ when 'link'
109
+ if 0 == recursion_limit
110
+ raise MaxSymlinkDepthExceededError.new(absolute_target)
111
+ else
112
+ self.resolve_symlink(absolute_target, recursion_limit-1)
113
+ end
114
+ when 'file', 'directory'
115
+ absolute_target
116
+ else
117
+ raise UnsupportedFileTypeError.new(absolute_target)
118
+ end
119
+ end
120
+ module_function :resolve_symlink
121
+
122
+ end
123
+
124
+ end