guard 1.4.0 → 2.18.0

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.
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