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