guard 1.4.0 → 2.18.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (89) hide show
  1. checksums.yaml +7 -0
  2. data/CHANGELOG.md +1 -677
  3. data/LICENSE +4 -2
  4. data/README.md +91 -753
  5. data/bin/_guard-core +11 -0
  6. data/bin/guard +108 -3
  7. data/lib/guard/aruba_adapter.rb +59 -0
  8. data/lib/guard/cli/environments/bundler.rb +22 -0
  9. data/lib/guard/cli/environments/evaluate_only.rb +35 -0
  10. data/lib/guard/cli/environments/valid.rb +69 -0
  11. data/lib/guard/cli.rb +129 -128
  12. data/lib/guard/commander.rb +104 -0
  13. data/lib/guard/commands/all.rb +37 -0
  14. data/lib/guard/commands/change.rb +31 -0
  15. data/lib/guard/commands/notification.rb +26 -0
  16. data/lib/guard/commands/pause.rb +29 -0
  17. data/lib/guard/commands/reload.rb +36 -0
  18. data/lib/guard/commands/scope.rb +38 -0
  19. data/lib/guard/commands/show.rb +24 -0
  20. data/lib/guard/config.rb +18 -0
  21. data/lib/guard/deprecated/dsl.rb +45 -0
  22. data/lib/guard/deprecated/evaluator.rb +39 -0
  23. data/lib/guard/deprecated/guard.rb +328 -0
  24. data/lib/guard/deprecated/guardfile.rb +84 -0
  25. data/lib/guard/deprecated/watcher.rb +27 -0
  26. data/lib/guard/dsl.rb +332 -363
  27. data/lib/guard/dsl_describer.rb +132 -122
  28. data/lib/guard/dsl_reader.rb +51 -0
  29. data/lib/guard/group.rb +34 -14
  30. data/lib/guard/guardfile/evaluator.rb +232 -0
  31. data/lib/guard/guardfile/generator.rb +128 -0
  32. data/lib/guard/guardfile.rb +24 -60
  33. data/lib/guard/interactor.rb +31 -255
  34. data/lib/guard/internals/debugging.rb +68 -0
  35. data/lib/guard/internals/groups.rb +40 -0
  36. data/lib/guard/internals/helpers.rb +13 -0
  37. data/lib/guard/internals/plugins.rb +53 -0
  38. data/lib/guard/internals/queue.rb +51 -0
  39. data/lib/guard/internals/scope.rb +121 -0
  40. data/lib/guard/internals/session.rb +180 -0
  41. data/lib/guard/internals/state.rb +25 -0
  42. data/lib/guard/internals/tracing.rb +33 -0
  43. data/lib/guard/internals/traps.rb +10 -0
  44. data/lib/guard/jobs/base.rb +21 -0
  45. data/lib/guard/jobs/pry_wrapper.rb +336 -0
  46. data/lib/guard/jobs/sleep.rb +26 -0
  47. data/lib/guard/notifier.rb +46 -212
  48. data/lib/guard/options.rb +22 -0
  49. data/lib/guard/plugin.rb +303 -0
  50. data/lib/guard/plugin_util.rb +191 -0
  51. data/lib/guard/rake_task.rb +42 -0
  52. data/lib/guard/runner.rb +80 -140
  53. data/lib/guard/templates/Guardfile +14 -0
  54. data/lib/guard/terminal.rb +13 -0
  55. data/lib/guard/ui/colors.rb +56 -0
  56. data/lib/guard/ui/config.rb +70 -0
  57. data/lib/guard/ui/logger.rb +30 -0
  58. data/lib/guard/ui.rb +163 -128
  59. data/lib/guard/version.rb +1 -2
  60. data/lib/guard/watcher/pattern/deprecated_regexp.rb +45 -0
  61. data/lib/guard/watcher/pattern/match_result.rb +18 -0
  62. data/lib/guard/watcher/pattern/matcher.rb +33 -0
  63. data/lib/guard/watcher/pattern/pathname_path.rb +15 -0
  64. data/lib/guard/watcher/pattern/simple_path.rb +23 -0
  65. data/lib/guard/watcher/pattern.rb +24 -0
  66. data/lib/guard/watcher.rb +52 -95
  67. data/lib/guard.rb +108 -376
  68. data/lib/tasks/releaser.rb +116 -0
  69. data/man/guard.1 +12 -9
  70. data/man/guard.1.html +18 -12
  71. metadata +148 -77
  72. data/images/guard.png +0 -0
  73. data/lib/guard/guard.rb +0 -156
  74. data/lib/guard/hook.rb +0 -120
  75. data/lib/guard/interactors/coolline.rb +0 -64
  76. data/lib/guard/interactors/helpers/completion.rb +0 -32
  77. data/lib/guard/interactors/helpers/terminal.rb +0 -46
  78. data/lib/guard/interactors/readline.rb +0 -94
  79. data/lib/guard/interactors/simple.rb +0 -19
  80. data/lib/guard/notifiers/emacs.rb +0 -69
  81. data/lib/guard/notifiers/gntp.rb +0 -118
  82. data/lib/guard/notifiers/growl.rb +0 -99
  83. data/lib/guard/notifiers/growl_notify.rb +0 -92
  84. data/lib/guard/notifiers/libnotify.rb +0 -96
  85. data/lib/guard/notifiers/notifysend.rb +0 -84
  86. data/lib/guard/notifiers/rb_notifu.rb +0 -102
  87. data/lib/guard/notifiers/terminal_notifier.rb +0 -66
  88. data/lib/guard/notifiers/tmux.rb +0 -69
  89. data/lib/guard/version.rbc +0 -130
data/lib/guard.rb CHANGED
@@ -1,437 +1,169 @@
1
- require 'thread'
2
- require 'listen'
1
+ require "thread"
2
+ require "listen"
3
+
4
+ require "guard/config"
5
+ require "guard/deprecated/guard" unless Guard::Config.new.strict?
6
+ require "guard/internals/helpers"
7
+
8
+ require "guard/internals/debugging"
9
+ require "guard/internals/traps"
10
+ require "guard/internals/queue"
11
+
12
+ # TODO: remove this class altogether
13
+ require "guard/interactor"
3
14
 
4
15
  # Guard is the main module for all Guard related modules and classes.
5
16
  # Also Guard plugins should use this namespace.
6
- #
7
17
  module Guard
18
+ Deprecated::Guard.add_deprecated(self) unless Config.new.strict?
8
19
 
9
- require 'guard/dsl'
10
- require 'guard/guardfile'
11
- require 'guard/group'
12
- require 'guard/interactor'
13
- require 'guard/notifier'
14
- require 'guard/runner'
15
- require 'guard/ui'
16
- require 'guard/watcher'
17
-
18
- # The Guardfile template for `guard init`
19
- GUARDFILE_TEMPLATE = File.expand_path('../guard/templates/Guardfile', __FILE__)
20
+ class << self
21
+ attr_reader :state
22
+ attr_reader :queue
23
+ attr_reader :listener
24
+ attr_reader :interactor
20
25
 
21
- # The location of user defined templates
22
- HOME_TEMPLATES = File.expand_path('~/.guard/templates')
26
+ # @private api
23
27
 
24
- class << self
25
- attr_accessor :options, :interactor, :runner, :listener, :lock
28
+ include Internals::Helpers
26
29
 
27
- # Initialize the Guard singleton:
30
+ # Initializes the Guard singleton:
28
31
  #
29
- # - Initialize the internal Guard state.
30
- # - Create the interactor when necessary for user interaction.
31
- # - Select and initialize the file change listener.
32
+ # * Initialize the internal Guard state;
33
+ # * Create the interactor
34
+ # * Select and initialize the file change listener.
32
35
  #
33
36
  # @option options [Boolean] clear if auto clear the UI should be done
34
37
  # @option options [Boolean] notify if system notifications should be shown
35
38
  # @option options [Boolean] debug if debug output should be shown
36
39
  # @option options [Array<String>] group the list of groups to start
37
- # @option options [String] watchdir the director to watch
40
+ # @option options [Array<String>] watchdir the directories to watch
38
41
  # @option options [String] guardfile the path to the Guardfile
39
- # @deprecated @option options [Boolean] watch_all_modifications watches all file modifications if true
40
- # @deprecated @option options [Boolean] no_vendor ignore vendored dependencies
41
42
  #
42
- def setup(options = {})
43
- @lock = Mutex.new
44
- @options = options
45
- @watchdir = (options[:watchdir] && File.expand_path(options[:watchdir])) || Dir.pwd
46
- @runner = ::Guard::Runner.new
47
- @allow_stop = Listen::Turnstile.new
43
+ # @return [Guard] the Guard singleton
44
+ def setup(cmdline_options = {})
45
+ init(cmdline_options)
48
46
 
49
- ::Guard::UI.clear(:force => true)
50
- deprecated_options_warning
47
+ @queue = Internals::Queue.new(Guard)
51
48
 
52
- setup_groups
53
- setup_guards
54
- setup_listener
55
- setup_signal_traps
56
-
57
- debug_command_execution if @options[:debug]
58
-
59
- ::Guard::Dsl.evaluate_guardfile(options)
60
- ::Guard::UI.error 'No guards found in Guardfile, please add at least one.' if @guards.empty?
61
-
62
- runner.deprecation_warning if @options[:show_deprecations]
63
-
64
- setup_notifier
65
- setup_interactor
66
-
67
- self
68
- end
49
+ _evaluate(state.session.evaluator_options)
69
50
 
70
- # Initialize the groups array with the `:default` group.
71
- #
72
- # @see Guard.groups
73
- #
74
- def setup_groups
75
- @groups = [Group.new(:default)]
76
- end
51
+ # NOTE: this should be *after* evaluate so :directories can work
52
+ # TODO: move listener setup to session?
53
+ @listener = Listen.send(*state.session.listener_args, &_listener_callback)
77
54
 
78
- # Initialize the guards array to an empty array.
79
- #
80
- # @see Guard.guards
81
- #
82
- def setup_guards
83
- @guards = []
84
- end
55
+ ignores = state.session.guardfile_ignore
56
+ @listener.ignore(ignores) unless ignores.empty?
85
57
 
86
- # Sets up traps to catch signals used to control Guard.
87
- #
88
- # Currently two signals are caught:
89
- # - `USR1` which pauses listening to changes.
90
- # - `USR2` which resumes listening to changes.
91
- #
92
- def setup_signal_traps
93
- unless defined?(JRUBY_VERSION)
94
- if Signal.list.keys.include?('USR1')
95
- Signal.trap('USR1') { ::Guard.pause unless @listener.paused? }
96
- end
97
-
98
- if Signal.list.keys.include?('USR2')
99
- Signal.trap('USR2') { ::Guard.pause if @listener.paused? }
100
- end
101
- end
102
- end
58
+ ignores = state.session.guardfile_ignore_bang
59
+ @listener.ignore!(ignores) unless ignores.empty?
103
60
 
104
- # Initializes the listener and registers a callback for changes.
105
- #
106
- def setup_listener
107
- listener_callback = lambda do |modified, added, removed|
108
- ::Guard::Dsl.reevaluate_guardfile if ::Guard::Watcher.match_guardfile?(modified)
61
+ Notifier.connect(state.session.notify_options)
109
62
 
110
- ::Guard.within_preserved_state do
111
- runner.run_on_changes(modified, added, removed)
112
- end
113
- end
63
+ traps = Internals::Traps
64
+ traps.handle("USR1") { async_queue_add([:guard_pause, :paused]) }
65
+ traps.handle("USR2") { async_queue_add([:guard_pause, :unpaused]) }
114
66
 
115
- listener_options = { :relative_paths => true }
116
- %w[latency force_polling].each do |option|
117
- listener_options[option.to_sym] = options[option] if options.key?(option)
118
- end
67
+ @interactor = Interactor.new(state.session.interactor_name == :sleep)
68
+ traps.handle("INT") { @interactor.handle_interrupt }
119
69
 
120
- @listener = Listen.to(@watchdir, listener_options).change(&listener_callback)
70
+ self
121
71
  end
122
72
 
123
- # Enables or disables the notifier based on user's configurations.
124
- #
125
- def setup_notifier
126
- options[:notify] && ENV['GUARD_NOTIFY'] != 'false' ? ::Guard::Notifier.turn_on : ::Guard::Notifier.turn_off
73
+ def init(cmdline_options)
74
+ @state = Internals::State.new(cmdline_options)
127
75
  end
128
76
 
129
- # Initializes the interactor unless the user has specified not to.
77
+ # Asynchronously trigger changes
130
78
  #
131
- def setup_interactor
132
- unless options[:no_interactions]
133
- @interactor = ::Guard::Interactor.fabricate
134
- end
135
- end
136
-
137
- # Start Guard by evaluating the `Guardfile`, initializing declared Guard plugins
138
- # and starting the available file change listener.
139
- # Main method for Guard that is called from the CLI when Guard starts.
79
+ # Currently supported args:
140
80
  #
141
- # - Setup Guard internals
142
- # - Evaluate the `Guardfile`
143
- # - Configure Notifiers
144
- # - Initialize the declared Guard plugins
145
- # - Start the available file change listener
81
+ # @example Old style hash:
82
+ # async_queue_add(modified: ['foo'], added: ['bar'], removed: [])
146
83
  #
147
- # @option options [Boolean] clear if auto clear the UI should be done
148
- # @option options [Boolean] notify if system notifications should be shown
149
- # @option options [Boolean] debug if debug output should be shown
150
- # @option options [Array<String>] group the list of groups to start
151
- # @option options [String] watchdir the director to watch
152
- # @option options [String] guardfile the path to the Guardfile
84
+ # @example New style signals with args:
85
+ # async_queue_add([:guard_pause, :unpaused ])
153
86
  #
154
- def start(options = {})
155
- setup(options)
156
- ::Guard::UI.info "Guard is now watching at '#{ @watchdir }'"
87
+ def async_queue_add(changes)
88
+ @queue << changes
157
89
 
158
- within_preserved_state do
159
- runner.run(:start)
160
- end
161
-
162
- listener.start(false)
163
-
164
- @allow_stop.wait if @allow_stop
90
+ # Putting interactor in background puts guard into foreground
91
+ # so it can handle change notifications
92
+ Thread.new { interactor.background }
165
93
  end
166
94
 
167
- # Stop Guard listening to file changes
168
- #
169
- def stop
170
- listener.stop
171
- interactor.stop if interactor
172
- runner.run(:stop)
173
- ::Guard::UI.info 'Bye bye...', :reset => true
95
+ private
174
96
 
175
- @allow_stop.signal if @allow_stop
97
+ # Check if any of the changes are actually watched for
98
+ # TODO: why iterate twice? reuse this info when running tasks
99
+ def _relevant_changes?(changes)
100
+ # TODO: no coverage!
101
+ files = changes.values.flatten(1)
102
+ scope = Guard.state.scope
103
+ watchers = scope.grouped_plugins.map do |_group, plugins|
104
+ plugins.map(&:watchers).flatten
105
+ end.flatten
106
+ watchers.any? { |watcher| files.any? { |file| watcher.match(file) } }
176
107
  end
177
108
 
178
- # Reload Guardfile and all Guard plugins currently enabled.
179
- #
180
- # @param [Hash] scopes hash with a Guard plugin or a group scope
181
- #
182
- def reload(scopes = {})
183
- within_preserved_state do
184
- ::Guard::UI.clear(:force => true)
185
- ::Guard::UI.action_with_scopes('Reload', scopes)
186
- ::Guard::Dsl.reevaluate_guardfile if scopes.empty?
187
- runner.run(:reload, scopes)
188
- end
109
+ def _relative_pathnames(paths)
110
+ paths.map { |path| _relative_pathname(path) }
189
111
  end
190
112
 
191
- # Trigger `run_all` on all Guard plugins currently enabled.
192
- #
193
- # @param [Hash] scopes hash with a Guard plugin or a group scope
194
- #
195
- def run_all(scopes = {})
196
- within_preserved_state do
197
- ::Guard::UI.clear(:force => true)
198
- ::Guard::UI.action_with_scopes('Run', scopes)
199
- runner.run(:run_all, scopes)
200
- end
201
- end
113
+ def _listener_callback
114
+ lambda do |modified, added, removed|
115
+ relative_paths = {
116
+ modified: _relative_pathnames(modified),
117
+ added: _relative_pathnames(added),
118
+ removed: _relative_pathnames(removed)
119
+ }
202
120
 
203
- # Pause Guard listening to file changes.
204
- #
205
- def pause
206
- if listener.paused?
207
- ::Guard::UI.info 'Un-paused files modification listening', :reset => true
208
- listener.unpause
209
- else
210
- ::Guard::UI.info 'Paused files modification listening', :reset => true
211
- listener.pause
212
- end
213
- end
121
+ _guardfile_deprecated_check(relative_paths[:modified])
214
122
 
215
- # Smart accessor for retrieving a specific Guard plugin or several Guard plugins at once.
216
- #
217
- # @see Guard.groups
218
- #
219
- # @example Filter Guard plugins by String or Symbol
220
- # Guard.guards('rspec')
221
- # Guard.guards(:rspec)
222
- #
223
- # @example Filter Guard plugins by Regexp
224
- # Guard.guards(/rsp.+/)
225
- #
226
- # @example Filter Guard plugins by Hash
227
- # Guard.guards({ :name => 'rspec', :group => 'backend' })
228
- #
229
- # @param [String, Symbol, Regexp, Hash] filter the filter to apply to the Guard plugins
230
- # @return [Array<Guard>] the filtered Guard plugins
231
- #
232
- def guards(filter = nil)
233
- @guards ||= []
234
-
235
- case filter
236
- when String, Symbol
237
- @guards.find { |guard| guard.class.to_s.downcase.sub('guard::', '') == filter.to_s.downcase.gsub('-', '') }
238
- when Regexp
239
- @guards.find_all { |guard| guard.class.to_s.downcase.sub('guard::', '') =~ filter }
240
- when Hash
241
- filter.inject(@guards) do |matches, (k, v)|
242
- if k.to_sym == :name
243
- matches.find_all { |guard| guard.class.to_s.downcase.sub('guard::', '') == v.to_s.downcase.gsub('-', '') }
244
- else
245
- matches.find_all { |guard| guard.send(k).to_sym == v.to_sym }
246
- end
247
- end
248
- else
249
- @guards
123
+ async_queue_add(relative_paths) if _relevant_changes?(relative_paths)
250
124
  end
251
125
  end
252
126
 
253
- # Smart accessor for retrieving a specific plugin group or several plugin groups at once.
254
- #
255
- # @see Guard.guards
256
- #
257
- # @example Filter groups by String or Symbol
258
- # Guard.groups('backend')
259
- # Guard.groups(:backend)
260
- #
261
- # @example Filter groups by Regexp
262
- # Guard.groups(/(back|front)end/)
263
- #
264
- # @param [String, Symbol, Regexp] filter the filter to apply to the Groups
265
- # @return [Array<Group>] the filtered groups
266
- #
267
- def groups(filter = nil)
268
- case filter
269
- when String, Symbol
270
- @groups.find { |group| group.name == filter.to_sym }
271
- when Regexp
272
- @groups.find_all { |group| group.name.to_s =~ filter }
273
- else
274
- @groups
275
- end
127
+ # TODO: obsoleted? (move to Dsl?)
128
+ def _pluginless_guardfile?
129
+ state.session.plugins.all.empty?
276
130
  end
277
131
 
278
- # Add a Guard plugin to use.
279
- #
280
- # @param [String] name the Guard name
281
- # @param [Array<Watcher>] watchers the list of declared watchers
282
- # @param [Array<Hash>] callbacks the list of callbacks
283
- # @param [Hash] options the plugin options (see the given Guard documentation)
284
- # @return [Guard::Guard] the added Guard plugin
285
- #
286
- def add_guard(name, watchers = [], callbacks = [], options = {})
287
- if name.to_sym == :ego
288
- ::Guard::UI.deprecation('Guard::Ego is now part of Guard. You can remove it from your Guardfile.')
289
- else
290
- guard_class = get_guard_class(name)
291
- callbacks.each { |callback| Hook.add_callback(callback[:listener], guard_class, callback[:events]) }
292
- guard = guard_class.new(watchers, options)
293
- @guards << guard
294
- guard
295
- end
296
- end
132
+ def _evaluate(options)
133
+ evaluator = Guardfile::Evaluator.new(options)
134
+ evaluator.evaluate
297
135
 
298
- # Add a Guard plugin group.
299
- #
300
- # @param [String] name the group name
301
- # @option options [Boolean] halt_on_fail if a task execution
302
- # should be halted for all Guard plugins in this group if one Guard throws `:task_has_failed`
303
- # @return [Guard::Group] the group added (or retrieved from the `@groups` variable if already present)
304
- #
305
- def add_group(name, options = {})
306
- group = groups(name)
307
- if group.nil?
308
- group = ::Guard::Group.new(name, options)
309
- @groups << group
310
- end
311
- group
312
- end
136
+ UI.reset_and_clear
313
137
 
314
- # Runs a block where the interactor is
315
- # blocked and execution is synchronized
316
- # to avoid state inconsistency.
317
- #
318
- # @yield the block to run
319
- #
320
- def within_preserved_state
321
- lock.synchronize do
322
- begin
323
- interactor.stop if interactor
324
- @result = yield
325
- rescue Interrupt
326
- end
327
-
328
- interactor.start if interactor
329
- end
330
- @result
331
- end
138
+ msg = "No plugins found in Guardfile, please add at least one."
139
+ UI.error msg if _pluginless_guardfile?
332
140
 
333
- # Tries to load the Guard plugin main class. This transforms the supplied Guard plugin
334
- # name into a class name:
335
- #
336
- # * `guardname` will become `Guard::Guardname`
337
- # * `dashed-guard-name` will become `Guard::DashedGuardName`
338
- # * `underscore_guard_name` will become `Guard::UnderscoreGuardName`
339
- #
340
- # When no class is found with the strict case sensitive rules, another
341
- # try is made to locate the class without matching case:
342
- #
343
- # * `rspec` will find a class `Guard::RSpec`
344
- #
345
- # @param [String] name the name of the Guard
346
- # @param [Boolean] fail_gracefully whether error messages should not be printed
347
- # @return [Class, nil] the loaded class
348
- #
349
- def get_guard_class(name, fail_gracefully=false)
350
- name = name.to_s
351
- try_require = false
352
- const_name = name.gsub(/\/(.?)/) { "::#{ $1.upcase }" }.gsub(/(?:^|[_-])(.)/) { $1.upcase }
353
- begin
354
- require "guard/#{ name.downcase }" if try_require
355
- self.const_get(self.constants.find { |c| c.to_s == const_name } || self.constants.find { |c| c.to_s.downcase == const_name.downcase })
356
- rescue TypeError
357
- unless try_require
358
- try_require = true
359
- retry
360
- else
361
- ::Guard::UI.error "Could not find class Guard::#{ const_name.capitalize }"
362
- end
363
- rescue LoadError => loadError
364
- unless fail_gracefully
365
- ::Guard::UI.error "Could not load 'guard/#{ name.downcase }' or find class Guard::#{ const_name.capitalize }"
366
- ::Guard::UI.error loadError.to_s
367
- end
141
+ if evaluator.inline?
142
+ UI.info("Using inline Guardfile.")
143
+ elsif evaluator.custom?
144
+ UI.info("Using Guardfile at #{ evaluator.path }.")
368
145
  end
146
+ rescue Guardfile::Evaluator::NoPluginsError => e
147
+ UI.error(e.message)
369
148
  end
370
149
 
371
- # Locate a path to a Guard plugin gem.
372
- #
373
- # @param [String] name the name of the Guard plugin without the prefix `guard-`
374
- # @return [String] the full path to the Guard gem
375
- #
376
- def locate_guard(name)
377
- if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
378
- Gem::Specification.find_by_name("guard-#{ name }").full_gem_path
379
- else
380
- Gem.source_index.find_name("guard-#{ name }").last.full_gem_path
381
- end
382
- rescue
383
- ::Guard::UI.error "Could not find 'guard-#{ name }' gem path."
384
- end
150
+ # TODO: remove at some point
151
+ # TODO: not tested because collides with ongoing refactoring
152
+ def _guardfile_deprecated_check(modified)
153
+ modified.map!(&:to_s)
154
+ regexp = %r{^(?:.+/)?Guardfile$}
155
+ guardfiles = modified.select { |path| regexp.match(path) }
156
+ return if guardfiles.empty?
385
157
 
386
- # Returns a list of Guard plugin Gem names installed locally.
387
- #
388
- # @return [Array<String>] a list of Guard plugin gem names
389
- #
390
- def guard_gem_names
391
- if Gem::Version.create(Gem::VERSION) >= Gem::Version.create('1.8.0')
392
- Gem::Specification.find_all.select { |x| x.name =~ /^guard-/ }
393
- else
394
- Gem.source_index.find_name(/^guard-/)
395
- end.map { |x| x.name.sub(/^guard-/, '') }
396
- end
397
-
398
- # Adds a command logger in debug mode. This wraps common command
399
- # execution functions and logs the executed command before execution.
400
- #
401
- def debug_command_execution
402
- Kernel.send(:alias_method, :original_system, :system)
403
- Kernel.send(:define_method, :system) do |command, *args|
404
- ::Guard::UI.debug "Command execution: #{ command } #{ args.join(' ') }"
405
- original_system command, *args
158
+ guardfile = Pathname("Guardfile").realpath
159
+ real_guardfiles = guardfiles.detect do |path|
160
+ /^Guardfile$/.match(path) || Pathname(path).expand_path == guardfile
406
161
  end
407
162
 
408
- Kernel.send(:alias_method, :original_backtick, :'`')
409
- Kernel.send(:define_method, :'`') do |command|
410
- ::Guard::UI.debug "Command execution: #{ command }"
411
- original_backtick command
412
- end
413
- end
163
+ return unless real_guardfiles
414
164
 
415
- # Deprecation message for the `watch_all_modifications` start option
416
- WATCH_ALL_MODIFICATIONS_DEPRECATION = <<-EOS.gsub(/^\s*/, '')
417
- Starting with Guard v1.1 the 'watch_all_modifications' option is removed and is now always on.
418
- EOS
419
-
420
- # Deprecation message for the `no_vendor` start option
421
- NO_VENDOR_DEPRECATION = <<-EOS.gsub(/^\s*/, '')
422
- Starting with Guard v1.1 the 'no_vendor' option is removed because the monitoring
423
- gems are now part of a new gem called Listen. (https://github.com/guard/listen)
424
-
425
- You can specify a custom version of any monitoring gem directly in your Gemfile
426
- if you want to overwrite Listen's default monitoring gems.
427
- EOS
428
-
429
- # Displays a warning for each deprecated options used.
430
- #
431
- def deprecated_options_warning
432
- ::Guard::UI.deprecation(WATCH_ALL_MODIFICATIONS_DEPRECATION) if options[:watch_all_modifications]
433
- ::Guard::UI.deprecation(NO_VENDOR_DEPRECATION) if options[:no_vendor]
165
+ UI.warning "Guardfile changed -- _guard-core will exit.\n"
166
+ exit 2 # nonzero to break any while loop
434
167
  end
435
-
436
168
  end
437
169
  end
@@ -0,0 +1,116 @@
1
+ # TODO: extract to gem?
2
+
3
+ class Releaser
4
+ def initialize(options = {})
5
+ @project_name = options.delete(:project_name) do
6
+ fail "project_name is needed!"
7
+ end
8
+
9
+ @gem_name = options.delete(:gem_name) do
10
+ fail "gem_name is needed!"
11
+ end
12
+
13
+ @github_repo = options.delete(:github_repo) do
14
+ fail "github_repo is needed!"
15
+ end
16
+
17
+ @version = options.delete(:version) do
18
+ fail "version is needed!"
19
+ end
20
+ end
21
+
22
+ def full
23
+ rubygems
24
+ github
25
+ end
26
+
27
+ def rubygems
28
+ input = nil
29
+ loop do
30
+ STDOUT.puts "Release #{@project_name} #{@version} to RubyGems? (y/n)"
31
+ input = STDIN.gets.chomp.downcase
32
+ break if %w(y n).include?(input)
33
+ end
34
+
35
+ exit if input == "n"
36
+
37
+ Rake::Task["release"].invoke
38
+ end
39
+
40
+ def github
41
+ tag_name = "v#{@version}"
42
+
43
+ require "gems"
44
+
45
+ _verify_released
46
+ _verify_tag_pushed
47
+
48
+ require "octokit"
49
+ gh_client = Octokit::Client.new(netrc: true)
50
+
51
+ gh_release = _detect_gh_release(gh_client, tag_name, true)
52
+ return unless gh_release
53
+
54
+ STDOUT.puts "Draft release for #{tag_name}:\n"
55
+ STDOUT.puts gh_release.body
56
+ STDOUT.puts "\n-------------------------\n\n"
57
+
58
+ _confirm_publish
59
+
60
+ return unless _update_release(gh_client, gh_release, tag_name)
61
+
62
+ gh_release = _detect_gh_release(gh_client, tag_name, false)
63
+
64
+ _success_summary(gh_release, tag_name)
65
+ end
66
+
67
+ private
68
+
69
+ def _verify_released
70
+ if @version != Gems.info(@gem_name)["version"]
71
+ STDOUT.puts "#{@project_name} #{@version} is not yet released."
72
+ STDOUT.puts "Please release it first with: rake release:gem"
73
+ exit
74
+ end
75
+ end
76
+
77
+ def _verify_tag_pushed
78
+ tags = `git ls-remote --tags origin`.split("\n")
79
+ return if tags.detect { |tag| tag =~ /v#{@version}$/ }
80
+
81
+ STDOUT.puts "The tag v#{@version} has not yet been pushed."
82
+ STDOUT.puts "Please push it first with: rake release:gem"
83
+ exit
84
+ end
85
+
86
+ def _success_summary(gh_release, tag_name)
87
+ href = gh_release.rels[:html].href
88
+ STDOUT.puts "GitHub release #{tag_name} has been published!"
89
+ STDOUT.puts "\nPlease enjoy and spread the word!"
90
+ STDOUT.puts "Lack of inspiration? Here's a tweet you could improve:\n\n"
91
+ STDOUT.puts "Just released #{@project_name} #{@version}! #{href}"
92
+ end
93
+
94
+ def _detect_gh_release(gh_client, tag_name, draft)
95
+ gh_releases = gh_client.releases(@github_repo)
96
+ gh_releases.detect { |r| r.tag_name == tag_name && r.draft == draft }
97
+ end
98
+
99
+ def _confirm_publish
100
+ input = nil
101
+ loop do
102
+ STDOUT.puts "Would you like to publish this GitHub release now? (y/n)"
103
+ input = STDIN.gets.chomp.downcase
104
+ break if %w(y n).include?(input)
105
+ end
106
+
107
+ exit if input == "n"
108
+ end
109
+
110
+ def _update_release(gh_client, gh_release, tag_name)
111
+ result = gh_client.update_release(gh_release.rels[:self].href, draft: false)
112
+ return true if result
113
+ STDOUT.puts "GitHub release #{tag_name} couldn't be published!"
114
+ false
115
+ end
116
+ end