again 1.1.4
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.
- data/.gitignore +5 -0
- data/Gemfile +4 -0
- data/README.md +201 -0
- data/Rakefile +1 -0
- data/again.gemspec +23 -0
- data/bin/again_test.rb +9 -0
- data/lib/again.rb +198 -0
- data/lib/again/extras.rb +17 -0
- data/lib/again/patches.rb +2 -0
- data/lib/again/patches/listen.rb +25 -0
- data/lib/again/patches/singleton.rb +17 -0
- data/lib/again/version.rb +3 -0
- metadata +90 -0
data/.gitignore
ADDED
data/Gemfile
ADDED
data/README.md
ADDED
@@ -0,0 +1,201 @@
|
|
1
|
+
# again.rb #
|
2
|
+
|
3
|
+
## DESCRIPTION ##
|
4
|
+
|
5
|
+
again is the best way to iteratively develop with Ruby.
|
6
|
+
|
7
|
+
It monitors your application's code or any required library
|
8
|
+
for changes. In the likely event of a change, the code is
|
9
|
+
automatically loaded again.
|
10
|
+
|
11
|
+
## Why? ##
|
12
|
+
|
13
|
+
After seeing a [live stream](http://www.youtube.com/watch?v=zdLDYUNRErQ&feature=player_detailpage#t=532s)
|
14
|
+
of Markus Persson [hyper-incrementally developing a game for a 48h game development competition in Java](
|
15
|
+
http://news.ycombinator.com/item?id=2911696), where he debugged
|
16
|
+
his 3d rendering code by just changing expressions, hitting save
|
17
|
+
and instantly seeing the effect, I wanted to have support for
|
18
|
+
hyper-iterative development in Ruby.
|
19
|
+
|
20
|
+
Change a color, save, switch to your application and it's changed.
|
21
|
+
|
22
|
+
again has been tested with Ruby 1.8, Ruby 1.9 and JRuby 1.6.2.
|
23
|
+
I use it for my personal development, including [Sinatra](
|
24
|
+
http://www.sinatrarb.com/) web development as well as
|
25
|
+
[Ruby/Gosu](http://www.libgosu.org/) game development.
|
26
|
+
|
27
|
+
(If anybody has a better link for the video of Markus Persson working incrementally,
|
28
|
+
[please drop me a mail](mailto:florian.s.gross@web.de)!)
|
29
|
+
|
30
|
+
|
31
|
+
## INSTALL ##
|
32
|
+
|
33
|
+
gem install again
|
34
|
+
|
35
|
+
|
36
|
+
## SYNOPSIS ##
|
37
|
+
|
38
|
+
### Getting started with again ###
|
39
|
+
|
40
|
+
All you need to do to setup automatic reloading of your
|
41
|
+
code is to require again:
|
42
|
+
|
43
|
+
```ruby
|
44
|
+
require 'again' # ...and again and again
|
45
|
+
```
|
46
|
+
|
47
|
+
You should do this after all other libraries of your application
|
48
|
+
have already been loaded: Make it your last `require`.
|
49
|
+
|
50
|
+
|
51
|
+
### Controlling what to execute again ###
|
52
|
+
|
53
|
+
#### Not running start-up logic again ####
|
54
|
+
|
55
|
+
When again detects a change to a file, it will reexecute all code
|
56
|
+
in that file. This also applies to the main file of your application,
|
57
|
+
the one you actually run from the command line.
|
58
|
+
|
59
|
+
For method and class definitions, this is no problem and things will
|
60
|
+
work exactly as you expect.
|
61
|
+
|
62
|
+
You should however wrap the actual start-up logic of your application
|
63
|
+
like this:
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# Current file being executed from command line?
|
67
|
+
if __FILE__ == $PROGRAM_NAME then
|
68
|
+
# Show them windows
|
69
|
+
window = GameWindow.new
|
70
|
+
window.show
|
71
|
+
end
|
72
|
+
```
|
73
|
+
|
74
|
+
This is something that is good style anyway (in order to allow your
|
75
|
+
code to be used as a library): The above would only be executed on
|
76
|
+
direct execution from the command line; and not when the file is
|
77
|
+
loaded as a library. [More detailed explanation of `__FILE__ == $PROGRAM_NAME`](http://blade.nagaokaut.ac.jp/cgi-bin/scat.rb/ruby/ruby-talk/184607)
|
78
|
+
|
79
|
+
again actually invests some extra effort to make sure that
|
80
|
+
`__FILE__ == $PROGRAM_NAME` blocks will not be run again
|
81
|
+
after reloading your main application file.
|
82
|
+
|
83
|
+
|
84
|
+
#### Running additional code on reloads ####
|
85
|
+
|
86
|
+
You can also have logic that is only run on reloads:
|
87
|
+
|
88
|
+
```ruby
|
89
|
+
if Again.reloaded? then
|
90
|
+
puts "Hello again"
|
91
|
+
end
|
92
|
+
```
|
93
|
+
|
94
|
+
This is useful when you want to reset your application into a
|
95
|
+
predictable state or when your frameworks need some special logic
|
96
|
+
to make sure that your changes will be picked up. (Such as clearing
|
97
|
+
routes in Sinatra, [see below](#sinatra-again).)
|
98
|
+
|
99
|
+
|
100
|
+
### gosu again ###
|
101
|
+
|
102
|
+
I'm using again for game development with Ruby, together with
|
103
|
+
[Ruby/Gosu](http://www.libgosu.org/). My code typically looks like this:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
require 'rubygems'
|
107
|
+
require 'gosu'
|
108
|
+
# ...
|
109
|
+
require 'again'
|
110
|
+
|
111
|
+
include Gosu
|
112
|
+
|
113
|
+
class GameWindow < Gosu::Window
|
114
|
+
def initialize()
|
115
|
+
@objects = ...
|
116
|
+
end
|
117
|
+
|
118
|
+
def update()
|
119
|
+
@objects.each(&:update)
|
120
|
+
rescue Exception => err
|
121
|
+
handle_error(err)
|
122
|
+
end
|
123
|
+
|
124
|
+
def draw()
|
125
|
+
@objects.each(&:draw)
|
126
|
+
rescue Exception => err
|
127
|
+
handle_error(err)
|
128
|
+
end
|
129
|
+
|
130
|
+
def handle_error(err)
|
131
|
+
# Simple logic, but it does the trick
|
132
|
+
STDERR.puts err, err.backtrace
|
133
|
+
sleep 3
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Current file being executed from command line?
|
138
|
+
if __FILE__ == $PROGRAM_NAME then
|
139
|
+
# Let's play
|
140
|
+
window = GameWindow.new
|
141
|
+
window.show
|
142
|
+
end
|
143
|
+
```
|
144
|
+
|
145
|
+
Rescuing exceptions in `update()` and `draw()` and
|
146
|
+
handling them in a central place is a very good idea.
|
147
|
+
If you have uncaught exceptions in there, Gosu will
|
148
|
+
shutdown your application. This would kill our nice
|
149
|
+
incremental development loop in the case of an error.
|
150
|
+
|
151
|
+
|
152
|
+
### sinatra again ###
|
153
|
+
|
154
|
+
I also use again to incrementalize my [Sinatra](http://www.libgosu.org/) development.
|
155
|
+
This is very similar to the above Gosu example, but with
|
156
|
+
one special twist:
|
157
|
+
|
158
|
+
```ruby
|
159
|
+
require 'rubygems'
|
160
|
+
require 'sinatra'
|
161
|
+
require 'json'
|
162
|
+
# ...
|
163
|
+
require 'again'
|
164
|
+
|
165
|
+
if Again.reloaded? then
|
166
|
+
# Here's the twist
|
167
|
+
Sinatra::Application.routes.clear
|
168
|
+
end
|
169
|
+
|
170
|
+
get '/' do
|
171
|
+
return "Awesome!"
|
172
|
+
end
|
173
|
+
```
|
174
|
+
|
175
|
+
We need to clear the Application routes first. Otherwise
|
176
|
+
sinatra won't let us redefine our routes. That's all!
|
177
|
+
|
178
|
+
|
179
|
+
## LICENSE ##
|
180
|
+
|
181
|
+
(The MIT License)
|
182
|
+
|
183
|
+
Copyright (c) 2012, Florian Gross <florian.s.gross@web.de>
|
184
|
+
|
185
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy of
|
186
|
+
this software and associated documentation files (the "Software"), to deal in
|
187
|
+
the Software without restriction, including without limitation the rights to
|
188
|
+
use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies
|
189
|
+
of the Software, and to permit persons to whom the Software is furnished to do
|
190
|
+
so, subject to the following conditions:
|
191
|
+
|
192
|
+
The above copyright notice and this permission notice shall be included in all
|
193
|
+
copies or substantial portions of the Software.
|
194
|
+
|
195
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
196
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
197
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
198
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
199
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
200
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
201
|
+
SOFTWARE.
|
data/Rakefile
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
data/again.gemspec
ADDED
@@ -0,0 +1,23 @@
|
|
1
|
+
# -*- encoding: utf-8 -*-
|
2
|
+
$:.push File.expand_path("../lib", __FILE__)
|
3
|
+
require "again/version"
|
4
|
+
|
5
|
+
Gem::Specification.new do |s|
|
6
|
+
s.name = "again"
|
7
|
+
s.version = Again::VERSION
|
8
|
+
|
9
|
+
s.authors = ["Florian Gross"]
|
10
|
+
s.email = ["Florian.S.Gross@web.de"]
|
11
|
+
s.homepage = "https://github.com/flgr/again"
|
12
|
+
s.summary = %q{The best way to iteratively develop with Ruby}
|
13
|
+
s.description = %q{again automatically reloads your application's code when it changes}
|
14
|
+
|
15
|
+
s.files = `git ls-files`.split("\n")
|
16
|
+
s.test_files = `git ls-files -- {test,spec,features}/*`.split("\n")
|
17
|
+
s.executables = [] # `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
|
18
|
+
s.require_paths = ["lib"]
|
19
|
+
|
20
|
+
# specify any dependencies here; for example:
|
21
|
+
# s.add_development_dependency "rspec"
|
22
|
+
s.add_runtime_dependency "listen"
|
23
|
+
end
|
data/bin/again_test.rb
ADDED
data/lib/again.rb
ADDED
@@ -0,0 +1,198 @@
|
|
1
|
+
require 'again/version'
|
2
|
+
require 'again/extras'
|
3
|
+
require 'again/patches'
|
4
|
+
|
5
|
+
require 'set'
|
6
|
+
require 'pathname'
|
7
|
+
require 'tmpdir'
|
8
|
+
require 'singleton'
|
9
|
+
require 'fileutils'
|
10
|
+
|
11
|
+
require 'rubygems'
|
12
|
+
require 'listen'
|
13
|
+
|
14
|
+
class Again
|
15
|
+
include Singleton
|
16
|
+
|
17
|
+
def self.start() self.instance end
|
18
|
+
def self.join() instance.join end
|
19
|
+
def self.reloaded?() instance.reloaded? end
|
20
|
+
|
21
|
+
def reloaded?() @reloaded end
|
22
|
+
|
23
|
+
def join()
|
24
|
+
# Can't simply join @watch_thread here as a refresh() can stop it.
|
25
|
+
# We want to survive refreshes so we instead join a thread which
|
26
|
+
# keeps checking for new watch_threads...
|
27
|
+
Thread.new do
|
28
|
+
loop do
|
29
|
+
if @watch_thread then
|
30
|
+
@watch_thread.alive?() ? @watch_thread.join : sleep(0.5)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end.join
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def with_reloaded()
|
39
|
+
old_reloaded, @reloaded = @reloaded, true
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
@reloaded = old_reloaded
|
43
|
+
end
|
44
|
+
|
45
|
+
def initialize()
|
46
|
+
@reloaded, @handler = false, nil
|
47
|
+
@watch_threads = ThreadGroup.new
|
48
|
+
@full_prog_name = File.expand_path($PROGRAM_NAME)
|
49
|
+
refresh()
|
50
|
+
end
|
51
|
+
|
52
|
+
def refresh()
|
53
|
+
refresh_libraries
|
54
|
+
refresh_handler
|
55
|
+
end
|
56
|
+
|
57
|
+
def refresh_libraries()
|
58
|
+
@base_paths = find_base_paths()
|
59
|
+
@libraries = find_libraries()
|
60
|
+
end
|
61
|
+
|
62
|
+
def find_features()
|
63
|
+
features = $LOADED_FEATURES.dup
|
64
|
+
|
65
|
+
# Allow realoding of main file (which is not a loaded library
|
66
|
+
# and thus does not appear in $LOADED_FEATURES)
|
67
|
+
features << @full_prog_name
|
68
|
+
|
69
|
+
# features doesn't include this file by default since
|
70
|
+
# technically it is still in the process of being loaded
|
71
|
+
# ...so manually include it
|
72
|
+
features << File.basename(__FILE__)
|
73
|
+
|
74
|
+
return features
|
75
|
+
end
|
76
|
+
|
77
|
+
# Avoid warnings on self-reload
|
78
|
+
remove_const(:JRUBY_PSEUDO_LIBS) if defined?(JRUBY_PSEUDO_LIBS)
|
79
|
+
remove_const(:PSEUDO_LIBS) if defined?(PSEUDO_LIBS)
|
80
|
+
|
81
|
+
# Probably incomplete...
|
82
|
+
JRUBY_PSEUDO_LIBS = %w(enumerable.jar enumerator.jar thread.rb tempfile.rb
|
83
|
+
java.rb jruby.rb socket.jar strscan.jar iconv.jar stringio.jar etc.jar
|
84
|
+
fcntl.rb timeout.rb digest/md5.jar jsignal_internal.rb rbconfig.rb
|
85
|
+
jruby/util.rb digest/sha1.jar digest.jar jruby/path_helper.rb)
|
86
|
+
|
87
|
+
PSEUDO_LIBS = %w(enumerable.so enumerator.so)
|
88
|
+
|
89
|
+
PSEUDO_LIBS.push(*JRUBY_PSEUDO_LIBS) if defined? JRUBY_VERSION
|
90
|
+
|
91
|
+
def pseudo_lib?(lib) PSEUDO_LIBS.include?(lib) end
|
92
|
+
|
93
|
+
RELOAD_MARKER = "--reload-temp--" unless defined?(RELOAD_MARKER)
|
94
|
+
|
95
|
+
def find_base_paths()
|
96
|
+
base_paths = *$LOAD_PATH.uniq
|
97
|
+
base_paths << File.dirname(@full_prog_name)
|
98
|
+
|
99
|
+
base_paths.map! { |path| File.expand_path(path) }
|
100
|
+
base_paths.reject! { |path| not File.exist?(path) or path.include?(RELOAD_MARKER) }
|
101
|
+
base_paths.uniq!
|
102
|
+
|
103
|
+
return base_paths
|
104
|
+
end
|
105
|
+
|
106
|
+
def find_libraries()
|
107
|
+
libraries, features = Set[], find_features
|
108
|
+
|
109
|
+
features.each do |rel_lib|
|
110
|
+
next if rel_lib.include?(RELOAD_MARKER)
|
111
|
+
|
112
|
+
res_lib = resolve_library(rel_lib)
|
113
|
+
|
114
|
+
if res_lib then
|
115
|
+
libraries << res_lib
|
116
|
+
elsif not pseudo_lib?(rel_lib) then
|
117
|
+
warn "Failed to resolve library %s to full path" % rel_lib
|
118
|
+
end
|
119
|
+
end
|
120
|
+
|
121
|
+
return libraries
|
122
|
+
end
|
123
|
+
|
124
|
+
def resolve_library(rel_lib)
|
125
|
+
return rel_lib if Pathname.new(rel_lib).absolute?
|
126
|
+
|
127
|
+
@base_paths.each do |path|
|
128
|
+
full_lib = File.join(path, rel_lib)
|
129
|
+
return full_lib if File.exist?(full_lib)
|
130
|
+
end
|
131
|
+
|
132
|
+
return nil
|
133
|
+
end
|
134
|
+
|
135
|
+
def refresh_handler()
|
136
|
+
# Our old @watch_thread will finish running after this
|
137
|
+
@handler.stop if @handler
|
138
|
+
|
139
|
+
@handler = Listen.to(*@base_paths)
|
140
|
+
@handler.change(&method(:on_change))
|
141
|
+
|
142
|
+
@watch_thread = Thread.new { @handler.start(true) }
|
143
|
+
end
|
144
|
+
|
145
|
+
def on_change(modified, added, removed)
|
146
|
+
(modified + added).each do |path|
|
147
|
+
reload(path) if @libraries.include?(path)
|
148
|
+
end
|
149
|
+
end
|
150
|
+
|
151
|
+
def reload(path)
|
152
|
+
STDERR.puts "Reloading %s" % path
|
153
|
+
|
154
|
+
# We don't want __FILE__ == $0 conditions to evaluate to true on reloads
|
155
|
+
# to avoid duplicate startup logic execution issues...
|
156
|
+
#
|
157
|
+
# So we intentionally create a copy of the file in that case to make
|
158
|
+
# those checks evaluate to false.
|
159
|
+
|
160
|
+
load_path = path
|
161
|
+
tmp_dir = nil
|
162
|
+
|
163
|
+
if @full_prog_name == path then
|
164
|
+
time = Time.now.to_i.to_s
|
165
|
+
tmp_dir = File.join(Dir.tmpdir, RELOAD_MARKER + "-dir-" + time)
|
166
|
+
Dir.mkdir(tmp_dir)
|
167
|
+
|
168
|
+
load_path = File.join(tmp_dir, RELOAD_MARKER + File.basename(load_path))
|
169
|
+
|
170
|
+
begin
|
171
|
+
# Try to hardlink first...
|
172
|
+
FileUtils.ln(path, load_path)
|
173
|
+
rescue Exception
|
174
|
+
# ...if that fails, create a copy.
|
175
|
+
FileUtils.copy(path, load_path)
|
176
|
+
end
|
177
|
+
end
|
178
|
+
|
179
|
+
begin
|
180
|
+
with_reloaded { load(load_path) }
|
181
|
+
rescue Exception => e
|
182
|
+
STDERR.puts e, "", e.backtrace.join("\n")
|
183
|
+
end
|
184
|
+
|
185
|
+
# need to do this from separate thread...
|
186
|
+
# we can not call handler.stop() from the thread
|
187
|
+
# that is getting the handler's callback
|
188
|
+
Thread.new { refresh }
|
189
|
+
ensure
|
190
|
+
FileUtils.rm_rf(tmp_dir) if tmp_dir
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
Again.start
|
195
|
+
|
196
|
+
if Again.reloaded? then
|
197
|
+
puts "Again reloaded"
|
198
|
+
end
|
data/lib/again/extras.rb
ADDED
@@ -0,0 +1,17 @@
|
|
1
|
+
class Again
|
2
|
+
def self.open_cmd()
|
3
|
+
(`which mate` rescue nil || "open").chomp
|
4
|
+
end
|
5
|
+
|
6
|
+
def self.open_lib(lib, cmd=open_cmd)
|
7
|
+
if lib = instance.resolve_library(lib) then
|
8
|
+
system(cmd, lib)
|
9
|
+
end
|
10
|
+
end
|
11
|
+
|
12
|
+
def self.open_libdir(lib, cmd=open_cmd)
|
13
|
+
if lib = instance.resolve_library(lib) then
|
14
|
+
system(cmd, File.dirname(lib))
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
require 'rubygems'
|
2
|
+
require 'listen'
|
3
|
+
|
4
|
+
class Listen::Adapter
|
5
|
+
def self.usable_and_works?(directories, options = {})
|
6
|
+
usable? #&& Array(directories).all? { |d| !writable?(d) || works?(d, options) }
|
7
|
+
end
|
8
|
+
|
9
|
+
def self.writable?(directory)
|
10
|
+
test_file = "#{directory}/.listen_test"
|
11
|
+
FileUtils.touch(test_file)
|
12
|
+
return true
|
13
|
+
rescue SystemCallError
|
14
|
+
return false
|
15
|
+
ensure
|
16
|
+
FileUtils.rm(test_file) if File.exists?(test_file)
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
# class Listen::DirectoryRecord
|
21
|
+
# # Don't use content-based modification at all (performance optimization)
|
22
|
+
# def content_modified?(path)
|
23
|
+
# return true
|
24
|
+
# end
|
25
|
+
# end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# Fix a bug in singleton: When the Singleton module is included multiple times
|
2
|
+
# in the same class, it will reset the instance variable on each inclusion.
|
3
|
+
#
|
4
|
+
# Together with again.rb this can easily result in lost state.
|
5
|
+
# So this version only sets the instance variable to nil if it was undefined.
|
6
|
+
|
7
|
+
require 'singleton'
|
8
|
+
|
9
|
+
class << Singleton
|
10
|
+
alias :included_without_guard :included
|
11
|
+
|
12
|
+
def included(klass)
|
13
|
+
return if klass.instance_variable_get(:@singleton_included)
|
14
|
+
included_without_guard(klass)
|
15
|
+
klass.instance_variable_set(:@singleton_included, true)
|
16
|
+
end
|
17
|
+
end
|
metadata
ADDED
@@ -0,0 +1,90 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: again
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
hash: 27
|
5
|
+
prerelease:
|
6
|
+
segments:
|
7
|
+
- 1
|
8
|
+
- 1
|
9
|
+
- 4
|
10
|
+
version: 1.1.4
|
11
|
+
platform: ruby
|
12
|
+
authors:
|
13
|
+
- Florian Gross
|
14
|
+
autorequire:
|
15
|
+
bindir: bin
|
16
|
+
cert_chain: []
|
17
|
+
|
18
|
+
date: 2012-07-02 00:00:00 Z
|
19
|
+
dependencies:
|
20
|
+
- !ruby/object:Gem::Dependency
|
21
|
+
name: listen
|
22
|
+
prerelease: false
|
23
|
+
requirement: &id001 !ruby/object:Gem::Requirement
|
24
|
+
none: false
|
25
|
+
requirements:
|
26
|
+
- - ">="
|
27
|
+
- !ruby/object:Gem::Version
|
28
|
+
hash: 3
|
29
|
+
segments:
|
30
|
+
- 0
|
31
|
+
version: "0"
|
32
|
+
type: :runtime
|
33
|
+
version_requirements: *id001
|
34
|
+
description: again automatically reloads your application's code when it changes
|
35
|
+
email:
|
36
|
+
- Florian.S.Gross@web.de
|
37
|
+
executables: []
|
38
|
+
|
39
|
+
extensions: []
|
40
|
+
|
41
|
+
extra_rdoc_files: []
|
42
|
+
|
43
|
+
files:
|
44
|
+
- .gitignore
|
45
|
+
- Gemfile
|
46
|
+
- README.md
|
47
|
+
- Rakefile
|
48
|
+
- again.gemspec
|
49
|
+
- bin/again_test.rb
|
50
|
+
- lib/again.rb
|
51
|
+
- lib/again/extras.rb
|
52
|
+
- lib/again/patches.rb
|
53
|
+
- lib/again/patches/listen.rb
|
54
|
+
- lib/again/patches/singleton.rb
|
55
|
+
- lib/again/version.rb
|
56
|
+
homepage: https://github.com/flgr/again
|
57
|
+
licenses: []
|
58
|
+
|
59
|
+
post_install_message:
|
60
|
+
rdoc_options: []
|
61
|
+
|
62
|
+
require_paths:
|
63
|
+
- lib
|
64
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
65
|
+
none: false
|
66
|
+
requirements:
|
67
|
+
- - ">="
|
68
|
+
- !ruby/object:Gem::Version
|
69
|
+
hash: 3
|
70
|
+
segments:
|
71
|
+
- 0
|
72
|
+
version: "0"
|
73
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
74
|
+
none: false
|
75
|
+
requirements:
|
76
|
+
- - ">="
|
77
|
+
- !ruby/object:Gem::Version
|
78
|
+
hash: 3
|
79
|
+
segments:
|
80
|
+
- 0
|
81
|
+
version: "0"
|
82
|
+
requirements: []
|
83
|
+
|
84
|
+
rubyforge_project:
|
85
|
+
rubygems_version: 1.8.11
|
86
|
+
signing_key:
|
87
|
+
specification_version: 3
|
88
|
+
summary: The best way to iteratively develop with Ruby
|
89
|
+
test_files: []
|
90
|
+
|