kicker 2.0.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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