kicker 2.0.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.
@@ -0,0 +1,10 @@
1
+ *.sw?
2
+ .DS_Store
3
+ coverage
4
+ rdoc
5
+ pkg
6
+ html/*.html
7
+ html/*.rid
8
+ html/*.css
9
+ html/classes
10
+ html/files
data/.kick ADDED
@@ -0,0 +1,20 @@
1
+ require 'ignore'
2
+
3
+ process do |files|
4
+ test_files = files.take_and_map do |file|
5
+ case file
6
+ when %r{^test/.+_test\.rb$}
7
+ file
8
+ when %r{^lib/kicker(\.rb|/validate\.rb|/growl\.rb)$}
9
+ ["test/initialization_test.rb", ("test/filesystem_change_test.rb" if $1 == '.rb')]
10
+ when %r{^lib/kicker/(.+)\.rb$}
11
+ "test/#{$1}_test.rb"
12
+ end
13
+ end
14
+
15
+ run_ruby_tests test_files
16
+ end
17
+
18
+ process do |files|
19
+ execute("rake docs:generate && open -a Safari html/index.html") if files.delete("README.rdoc")
20
+ end
data/LICENSE ADDED
@@ -0,0 +1,54 @@
1
+ Kicker:
2
+
3
+ Copyright (c) 2009 Eloy Duran <eloy.de.enige@gmail.com>
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
23
+
24
+ ======================================================================
25
+
26
+ Rucola: http://github.com/alloy/rucola/tree/master
27
+
28
+ Copyright (c) 2008 Eloy Duran <eloy.de.enige@gmail.com>
29
+
30
+ Permission is hereby granted, free of charge, to any person obtaining
31
+ a copy of this software and associated documentation files (the
32
+ "Software"), to deal in the Software without restriction, including
33
+ without limitation the rights to use, copy, modify, merge, publish,
34
+ distribute, sublicense, and/or sell copies of the Software, and to
35
+ permit persons to whom the Software is furnished to do so, subject to
36
+ the following conditions:
37
+
38
+ The above copyright notice and this permission notice shall be
39
+ included in all copies or substantial portions of the Software.
40
+
41
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
42
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
43
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
44
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
45
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
46
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
47
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
48
+
49
+ ======================================================================
50
+
51
+ growlnotifier: http://github.com/psychs/growlnotifier/tree/master
52
+
53
+ Copyright (c) 2007-2008 Satoshi Nakagawa <psychs@limechat.net>, Eloy Duran <e.duran@superalloy.nl>
54
+ You can redistribute it and/or modify it under the same terms as Ruby.
@@ -0,0 +1,141 @@
1
+ = Kicker
2
+
3
+ A lean, agnostic, flexible file-change watcher, using OS X FSEvents.
4
+
5
+ http://github.com/alloy/kicker/raw/master/html/images/kikker.jpg
6
+
7
+ Meet king kikker, kicking stuff in your computers is his dream come true!
8
+
9
+ <i>Drawing by Manfred Stienstra. The character is purely fictional, so if you
10
+ feel offended; live with it.</i>
11
+
12
+ == Installation
13
+
14
+ $ sudo gem install alloy-kicker -s http://gems.github.com
15
+
16
+ == The short version
17
+
18
+ Usage: kicker [options] [paths to watch]
19
+ -e, --execute [COMMAND] The command to execute.
20
+ --[no-]growl Whether or not to use Growl. Default is to use growl.
21
+ --growl-command [COMMAND] The command to execute when the Growl succeeded message is clicked.
22
+ -l, --latency [FLOAT] The time to collect file change events before acting on them. Defaults to 1.5 sec.
23
+ -r, --recipe [NAME] A named recipe to load.
24
+
25
+ Available recipes:
26
+ - ignore
27
+ - jstest
28
+ - rails
29
+
30
+ == The long version
31
+
32
+ === Execute a shell command
33
+
34
+ Show all files, whenever a change occurs in the current work directory:
35
+
36
+ $ kicker -e "ls -l" .
37
+
38
+ Show all files, whenever a change occurs to a specific file:
39
+
40
+ $ kicker -e "ls -l" foo.txt
41
+
42
+ Or use it as a ghetto-autotest, running tests whenever files change:
43
+
44
+ $ kicker -e "ruby test/test_case.rb" test/test_case.rb lib/file.rb
45
+
46
+ Et cetera.
47
+
48
+ === Using recipes
49
+
50
+ A recipe is a predefined handler. You can use as many as you like, by
51
+ specifying them with the <tt>--recipe</tt> (<tt>-r</tt>) option.
52
+
53
+ For instance, when in the root of a typical Ruby on Rails application, using
54
+ the <tt>rails</tt> recipe will map models, concerns, controllers, helpers, and
55
+ views to their respective test files. These will then all be ran with Ruby.
56
+
57
+ A few recipes come shipped with Kicker:
58
+ * Ruby on Rails, as aforementioned.
59
+ * JavaScript tests, needs
60
+ HeadlessSquirrel[http://github.com/Fingertips/Headless-squirrel] to run.
61
+ * Ignore, ignores logs, tmp, and svn and git files.
62
+
63
+ Add your own shared recipes to <tt>~/.kick</tt>.
64
+
65
+ === Project specific handlers
66
+
67
+ Most of the time, you’ll want to create handlers specific to the project at
68
+ hand. This can be done by adding your handlers to a <tt>.kick</tt> file and
69
+ running Kicker from the directory containing it.
70
+
71
+ This file is reloaded once saved. No need to stop Kicker.
72
+
73
+ == Writing handlers
74
+
75
+ Whenever file-change events occur, Kicker will go through a chain of handlers
76
+ until that the files list is empty, or the end of the chain is reached.
77
+
78
+ Handlers are objects that respond to <tt>#call</tt>. These are typically Proc
79
+ objects. (If you know Rack, you’re familiar with this concept.) Every handler
80
+ gets passed a list of changed files and can decide whether or not to act on
81
+ them. Normally when handling a file, you should remove it from the files list,
82
+ unless you want to let the file fall through to another handler. In the same
83
+ way, one can add files to handler to the files list.
84
+
85
+ ==== Time for a simple example
86
+
87
+ process do |files|
88
+ execute("rake docs:generate && open -a Safari html/index.html") if files.delete("README.rdoc")
89
+ end
90
+
91
+ A handler is defined by passing a block to <tt>process</tt>. Which is one of
92
+ three possible callback chains to add your handlers to, the others being:
93
+ <tt>pre_process</tt> and <tt>post_process</tt>. See Kernel for more info.
94
+
95
+ Then <tt>README.rdoc</tt> is deleted from the files array. If it did exist in
96
+ the array and was deleted, a shell command is executed which runs a rake task
97
+ to generate rdoc and open the docs with Safari.
98
+
99
+ ==== Something more elaborate.
100
+
101
+ Consider a Rails application with a mailer. Since the naming convention of
102
+ mailer views tend to be fairly application specific, a specific handler has to
103
+ be added:
104
+
105
+ process do |files|
106
+ test_files = files.take_and_map do |file|
107
+ if path =~ %r{^app/views/mailer/\w+\.erb$}
108
+ 'test/unit/mailer_test.rb'
109
+
110
+ # elsif ... handle more app specific stuff
111
+ end
112
+ end
113
+
114
+ run_ruby_tests test_files
115
+ end
116
+
117
+ The files list is iterated over with the Array#take_and_map method, which both
118
+ removes and maps the results. This is an easy way to do a common thing in
119
+ recipes. See Kicker::ArrayExt for details.
120
+
121
+ The handler then checks if the file is a mailer view and if so runs the
122
+ mailers test case. The <tt>run_ruby_tests</tt> runs them with the following
123
+ command:
124
+
125
+ execute "ruby -r #{test_files.join(' -r ')} -e ''" unless test_files.empty?
126
+
127
+ See Kernel for more info on the utility methods.
128
+
129
+ ==== Addendum
130
+
131
+ The recipes directory that ships with Kicker and <tt>~/.kick</tt> are both
132
+ added to the load path, so any recipes can be required. Once they’re required
133
+ they are added to the callback chains.
134
+
135
+ As an example, say you might want to ignore files in <tt>./data</tt>:
136
+
137
+ require 'ignore'
138
+ ignore(/^data\//)
139
+
140
+ That’s basically it, just remember that the order of specifying handlers _can_
141
+ be important in your decision on where to specify handlers.
@@ -0,0 +1,37 @@
1
+ require 'rubygems'
2
+ require 'rake'
3
+ require 'rake/testtask'
4
+ require 'rake/rdoctask'
5
+
6
+ begin
7
+ require 'jeweler'
8
+ Jeweler::Tasks.new do |gem|
9
+ gem.name = "kicker"
10
+ gem.summary = %Q{A lean, agnostic, flexible file-change watcher, using OS X FSEvents.}
11
+ gem.email = "eloy.de.enige@gmail.com"
12
+ gem.homepage = "http://github.com/alloy/kicker"
13
+ gem.authors = ["Eloy Duran"]
14
+ gem.executables << 'kicker'
15
+ gem.files.concat FileList['vendor/**/*']
16
+ gem.require_paths = ["lib", "vendor"]
17
+ gem.has_rdoc = true
18
+ end
19
+ rescue LoadError
20
+ puts "Jeweler not available. Install it with: sudo gem install technicalpickles-jeweler -s http://gems.github.com"
21
+ end
22
+
23
+ Rake::TestTask.new do |t|
24
+ t.libs << "test"
25
+ t.test_files = FileList['test/**/*_test.rb']
26
+ t.options = '-rs'
27
+ end
28
+
29
+ namespace :docs do
30
+ Rake::RDocTask.new('generate') do |t|
31
+ t.main = "README.rdoc"
32
+ t.rdoc_files.include("README.rdoc", "lib/**/*.rb")
33
+ t.options << '--charset=utf8'
34
+ end
35
+ end
36
+
37
+ task :default => :test
@@ -0,0 +1 @@
1
+ * Add a recipe which implements the basic autotest mapping API.
@@ -0,0 +1,4 @@
1
+ ---
2
+ :minor: 0
3
+ :patch: 0
4
+ :major: 2
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ $:.unshift File.expand_path('../../lib', __FILE__) if $0 == __FILE__
3
+
4
+ require 'kicker'
5
+ Kicker.run
Binary file
@@ -0,0 +1,95 @@
1
+ # Generated by jeweler
2
+ # DO NOT EDIT THIS FILE
3
+ # Instead, edit Jeweler::Tasks in Rakefile, and run `rake gemspec`
4
+ # -*- encoding: utf-8 -*-
5
+
6
+ Gem::Specification.new do |s|
7
+ s.name = %q{kicker}
8
+ s.version = "2.0.0"
9
+
10
+ s.required_rubygems_version = Gem::Requirement.new(">= 0") if s.respond_to? :required_rubygems_version=
11
+ s.authors = ["Eloy Duran"]
12
+ s.date = %q{2009-09-29}
13
+ s.email = %q{eloy.de.enige@gmail.com}
14
+ s.executables = ["kicker", "kicker"]
15
+ s.extra_rdoc_files = [
16
+ "LICENSE",
17
+ "README.rdoc"
18
+ ]
19
+ s.files = [
20
+ ".gitignore",
21
+ ".kick",
22
+ "LICENSE",
23
+ "README.rdoc",
24
+ "Rakefile",
25
+ "TODO.rdoc",
26
+ "VERSION.yml",
27
+ "bin/kicker",
28
+ "html/images/kikker.jpg",
29
+ "kicker.gemspec",
30
+ "lib/kicker.rb",
31
+ "lib/kicker/callback_chain.rb",
32
+ "lib/kicker/core_ext.rb",
33
+ "lib/kicker/growl.rb",
34
+ "lib/kicker/options.rb",
35
+ "lib/kicker/recipes/could_not_handle_file.rb",
36
+ "lib/kicker/recipes/dot_kick.rb",
37
+ "lib/kicker/recipes/execute_cli_command.rb",
38
+ "lib/kicker/recipes/ignore.rb",
39
+ "lib/kicker/recipes/jstest.rb",
40
+ "lib/kicker/recipes/rails.rb",
41
+ "lib/kicker/utils.rb",
42
+ "lib/kicker/validate.rb",
43
+ "test/callback_chain_test.rb",
44
+ "test/core_ext_test.rb",
45
+ "test/filesystem_change_test.rb",
46
+ "test/fixtures/a_file_thats_reloaded.rb",
47
+ "test/initialization_test.rb",
48
+ "test/options_test.rb",
49
+ "test/recipes/could_not_handle_file_test.rb",
50
+ "test/recipes/dot_kick_test.rb",
51
+ "test/recipes/execute_cli_command_test.rb",
52
+ "test/recipes/ignore_test.rb",
53
+ "test/recipes/jstest_test.rb",
54
+ "test/recipes/rails_test.rb",
55
+ "test/test_helper.rb",
56
+ "test/utils_test.rb",
57
+ "vendor/growlnotifier/growl.rb",
58
+ "vendor/growlnotifier/growl.rb",
59
+ "vendor/growlnotifier/growl_helpers.rb",
60
+ "vendor/growlnotifier/growl_helpers.rb",
61
+ "vendor/rucola/fsevents.rb",
62
+ "vendor/rucola/fsevents.rb"
63
+ ]
64
+ s.homepage = %q{http://github.com/alloy/kicker}
65
+ s.rdoc_options = ["--charset=UTF-8"]
66
+ s.require_paths = ["lib", "vendor"]
67
+ s.rubygems_version = %q{1.3.5}
68
+ s.summary = %q{A simple OS X CLI tool which uses FSEvents to run a given shell command.}
69
+ s.test_files = [
70
+ "test/callback_chain_test.rb",
71
+ "test/core_ext_test.rb",
72
+ "test/filesystem_change_test.rb",
73
+ "test/fixtures/a_file_thats_reloaded.rb",
74
+ "test/initialization_test.rb",
75
+ "test/options_test.rb",
76
+ "test/recipes/could_not_handle_file_test.rb",
77
+ "test/recipes/dot_kick_test.rb",
78
+ "test/recipes/execute_cli_command_test.rb",
79
+ "test/recipes/ignore_test.rb",
80
+ "test/recipes/jstest_test.rb",
81
+ "test/recipes/rails_test.rb",
82
+ "test/test_helper.rb",
83
+ "test/utils_test.rb"
84
+ ]
85
+
86
+ if s.respond_to? :specification_version then
87
+ current_version = Gem::Specification::CURRENT_SPECIFICATION_VERSION
88
+ s.specification_version = 3
89
+
90
+ if Gem::Version.new(Gem::RubyGemsVersion) >= Gem::Version.new('1.2.0') then
91
+ else
92
+ end
93
+ else
94
+ end
95
+ end
@@ -0,0 +1,135 @@
1
+ $:.unshift File.expand_path('../../vendor', __FILE__)
2
+ require 'rucola/fsevents'
3
+
4
+ require 'kicker/callback_chain'
5
+ require 'kicker/core_ext'
6
+ require 'kicker/growl'
7
+ require 'kicker/options'
8
+ require 'kicker/utils'
9
+ require 'kicker/validate'
10
+
11
+ RECIPES_DIR = File.expand_path('../kicker/recipes', __FILE__)
12
+ $:.unshift RECIPES_DIR
13
+ require 'could_not_handle_file'
14
+ require 'execute_cli_command'
15
+
16
+ USER_RECIPES_DIR = File.expand_path('~/.kick')
17
+ $:.unshift USER_RECIPES_DIR if File.exist?(USER_RECIPES_DIR)
18
+
19
+ class Kicker #:nodoc:
20
+ class << self
21
+ attr_accessor :latency
22
+
23
+ def latency
24
+ @latency ||= 1
25
+ end
26
+
27
+ def paths
28
+ @paths ||= %w{ . }
29
+ end
30
+
31
+ def run(argv = ARGV)
32
+ options = parse_options(argv)
33
+ load_recipes(options[:recipes]) if options[:recipes]
34
+ load_dot_kick
35
+ new(options).start
36
+ end
37
+
38
+ private
39
+
40
+ def load_dot_kick
41
+ if File.exist?('.kick')
42
+ require 'dot_kick'
43
+ ReloadDotKick.save_state
44
+ load '.kick'
45
+ end
46
+ end
47
+
48
+ def load_recipes(recipes)
49
+ recipes.each do |recipe|
50
+ raise "Recipe `#{recipe}' does not exist." unless recipe_exists?(recipe)
51
+ require recipe
52
+ end
53
+ end
54
+
55
+ def recipe_exists?(recipe)
56
+ File.exist?("#{RECIPES_DIR}/#{recipe}.rb") || File.exist?("#{USER_RECIPES_DIR}/#{recipe}.rb")
57
+ end
58
+ end
59
+
60
+ attr_reader :latency, :paths, :last_event_processed_at
61
+
62
+ def initialize(options)
63
+ @paths = (options[:paths] ? options[:paths] : Kicker.paths).map { |path| File.expand_path(path) }
64
+ @latency = options[:latency] || self.class.latency
65
+
66
+ self.class.use_growl = options[:growl]
67
+ self.class.growl_command = options[:growl_command]
68
+
69
+ finished_processing!
70
+ end
71
+
72
+ def start
73
+ validate_options!
74
+
75
+ log "Watching for changes on: #{@paths.join(', ')}"
76
+ log ''
77
+
78
+ run_watch_dog!
79
+ start_growl! if self.class.use_growl
80
+
81
+ OSX.CFRunLoopRun
82
+ end
83
+
84
+ private
85
+
86
+ def run_watch_dog!
87
+ dirs = @paths.map { |path| File.directory?(path) ? path : File.dirname(path) }
88
+ watch_dog = Rucola::FSEvents.start_watching(dirs, :latency => @latency) { |events| process(events) }
89
+
90
+ trap('INT') do
91
+ log "Exiting…"
92
+ watch_dog.stop
93
+ exit
94
+ end
95
+ end
96
+
97
+ def finished_processing!
98
+ @last_event_processed_at = Time.now
99
+ end
100
+
101
+ def process(events)
102
+ unless (files = changed_files(events)).empty?
103
+ full_chain.call(files)
104
+ finished_processing!
105
+ end
106
+ end
107
+
108
+ def changed_files(events)
109
+ make_paths_relative(events.map do |event|
110
+ files_in_directory(event.path).select { |file| file_changed_since_last_event? file }
111
+ end.flatten.uniq.sort)
112
+ end
113
+
114
+ def files_in_directory(dir)
115
+ Dir.entries(dir)[2..-1].map { |f| File.join(dir, f) }
116
+ end
117
+
118
+ def file_changed_since_last_event?(file)
119
+ File.mtime(file) > @last_event_processed_at
120
+ rescue Errno::ENOENT
121
+ false
122
+ end
123
+
124
+ def make_paths_relative(files)
125
+ return files if files.empty?
126
+ wd = Dir.pwd
127
+ files.map do |file|
128
+ if file[0..wd.length-1] == wd
129
+ file[wd.length+1..-1]
130
+ else
131
+ file
132
+ end
133
+ end
134
+ end
135
+ end