ergo 0.3.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.
- data/.index +62 -0
- data/.yardopts +10 -0
- data/HISTORY.md +47 -0
- data/LICENSE.txt +25 -0
- data/README.md +161 -0
- data/bin/ergo +4 -0
- data/demo/03_runner/01_applying_rules.md +51 -0
- data/demo/applique/ae.rb +1 -0
- data/demo/applique/ergo.rb +7 -0
- data/demo/overview.md +0 -0
- data/lib/ergo.rb +20 -0
- data/lib/ergo.yml +62 -0
- data/lib/ergo/book.rb +218 -0
- data/lib/ergo/cli.rb +185 -0
- data/lib/ergo/core_ext.rb +4 -0
- data/lib/ergo/core_ext/boolean.rb +10 -0
- data/lib/ergo/core_ext/cli.rb +56 -0
- data/lib/ergo/core_ext/true_class.rb +58 -0
- data/lib/ergo/digest.rb +196 -0
- data/lib/ergo/ignore.rb +146 -0
- data/lib/ergo/match.rb +26 -0
- data/lib/ergo/rule.rb +134 -0
- data/lib/ergo/runner.rb +377 -0
- data/lib/ergo/shellutils.rb +79 -0
- data/lib/ergo/state.rb +112 -0
- data/lib/ergo/system.rb +51 -0
- data/man/.gitignore +2 -0
- data/man/ergo.1.ronn +50 -0
- metadata +163 -0
data/lib/ergo/runner.rb
ADDED
@@ -0,0 +1,377 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
# Default rules file.
|
4
|
+
RULES_SCRIPT = ".ergo/script.rb"
|
5
|
+
|
6
|
+
# Runner is the main class which controls execution.
|
7
|
+
#
|
8
|
+
class Runner
|
9
|
+
|
10
|
+
# Initialize new Session instance.
|
11
|
+
#
|
12
|
+
# Returns nothing.
|
13
|
+
def initialize(options={})
|
14
|
+
@script = options[:script]
|
15
|
+
@system = options[:system]
|
16
|
+
|
17
|
+
self.root = options[:root]
|
18
|
+
self.trial = options[:trial]
|
19
|
+
self.fresh = options[:fresh]
|
20
|
+
self.watch = options[:watch]
|
21
|
+
self.ignore = options[:ignore]
|
22
|
+
|
23
|
+
@digests = {}
|
24
|
+
end
|
25
|
+
|
26
|
+
# Watch period, default is every 5 minutes.
|
27
|
+
#
|
28
|
+
# Returns [Fixnum]
|
29
|
+
def watch
|
30
|
+
@watch
|
31
|
+
end
|
32
|
+
|
33
|
+
# Set watch seconds. Minimum watch time is 1 second.
|
34
|
+
# Setting watch before calling #run creates a simple loop.
|
35
|
+
# It can eat up CPU cycles so use it wisely. A watch time
|
36
|
+
# of 4 seconds is a good time period. If you are patient
|
37
|
+
# go for 15 seconds or more.
|
38
|
+
#
|
39
|
+
# Returns [Fixnum,nil]
|
40
|
+
def watch=(seconds)
|
41
|
+
if seconds
|
42
|
+
seconds = seconds.to_i
|
43
|
+
seconds = 1 if seconds < 1
|
44
|
+
@watch = seconds
|
45
|
+
else
|
46
|
+
@watch = nil
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
# Nullify digest and make a fresh run?
|
51
|
+
#
|
52
|
+
# Returns [Boolean]
|
53
|
+
def fresh?
|
54
|
+
@fresh
|
55
|
+
end
|
56
|
+
|
57
|
+
# Set whether to nullify digest and make a fresh run.
|
58
|
+
#
|
59
|
+
# Returns [Boolean]
|
60
|
+
def fresh=(boolean)
|
61
|
+
@fresh = !! boolean
|
62
|
+
end
|
63
|
+
|
64
|
+
# Is this trial-run only?
|
65
|
+
#
|
66
|
+
# TODO: Trial mode is not implemented yet!
|
67
|
+
#
|
68
|
+
# Returns [Boolean]
|
69
|
+
def trial?
|
70
|
+
@trial
|
71
|
+
end
|
72
|
+
|
73
|
+
# Set trial run mode.
|
74
|
+
#
|
75
|
+
# Arguments
|
76
|
+
# bool - Flag for trial mode. [Boolean]
|
77
|
+
#
|
78
|
+
# Returns `bool` flag. [Boolean]
|
79
|
+
def trial=(bool)
|
80
|
+
@trial = !!bool
|
81
|
+
end
|
82
|
+
|
83
|
+
# Locate project root. This method ascends up the file system starting
|
84
|
+
# as the current working directory looking for a `.ergo` directory.
|
85
|
+
# When found, the directory in which it is found is returned as the root.
|
86
|
+
# It is also memoized, so repeated calls to this method will not repeat
|
87
|
+
# the search.
|
88
|
+
#
|
89
|
+
# Returns [String]
|
90
|
+
def root
|
91
|
+
dir = root?
|
92
|
+
raise RootError, "cannot locate project root" unless dir
|
93
|
+
dir
|
94
|
+
end
|
95
|
+
|
96
|
+
#
|
97
|
+
def root?
|
98
|
+
@root ||= (
|
99
|
+
r = nil
|
100
|
+
d = Dir.pwd
|
101
|
+
while d != home && d != '/'
|
102
|
+
if File.directory?('.ergo')
|
103
|
+
break r = d
|
104
|
+
end
|
105
|
+
d = File.dirname(d)
|
106
|
+
end
|
107
|
+
r
|
108
|
+
)
|
109
|
+
end
|
110
|
+
|
111
|
+
# Set the root directory.
|
112
|
+
#
|
113
|
+
# Returns [String]
|
114
|
+
def root=(dir)
|
115
|
+
@root = dir if dir
|
116
|
+
end
|
117
|
+
|
118
|
+
# Home directory.
|
119
|
+
#
|
120
|
+
# Returns [String]
|
121
|
+
def home
|
122
|
+
@home ||= File.expand_path('~')
|
123
|
+
end
|
124
|
+
|
125
|
+
# Instance of {Ergo::System}.
|
126
|
+
#
|
127
|
+
# Returns [System]
|
128
|
+
def system
|
129
|
+
@system ||= System.new(script)
|
130
|
+
end
|
131
|
+
|
132
|
+
# Rules script to load.
|
133
|
+
#
|
134
|
+
# Returns List of file paths. [Array]
|
135
|
+
def script
|
136
|
+
@script || (@system ? nil : Dir[RULES_SCRIPT].first)
|
137
|
+
end
|
138
|
+
|
139
|
+
# File globs to ignore.
|
140
|
+
#
|
141
|
+
# Returns [Ignore] instance.
|
142
|
+
#def digest
|
143
|
+
# @digest ||= Digest.new(:ignore=>ignore)
|
144
|
+
#end
|
145
|
+
|
146
|
+
# File globs to ignore.
|
147
|
+
#
|
148
|
+
# Returns [Ignore] instance.
|
149
|
+
def ignore
|
150
|
+
@ignore ||= Ignore.new(:root=>root)
|
151
|
+
end
|
152
|
+
|
153
|
+
# Set ignore.
|
154
|
+
def ignore=(file)
|
155
|
+
@ignore = Ignore.new(:root=>root, :file=>file)
|
156
|
+
end
|
157
|
+
|
158
|
+
# List of rules from the system.
|
159
|
+
#
|
160
|
+
# Returns [Array<Rule>]
|
161
|
+
def rules
|
162
|
+
system.rules
|
163
|
+
end
|
164
|
+
|
165
|
+
# Run rules.
|
166
|
+
#
|
167
|
+
# Returns nothing.
|
168
|
+
def run(*marks)
|
169
|
+
raise ArgumentError, "invalid bookmark" unless marks.all?{ |m| /\w+/ =~ m }
|
170
|
+
|
171
|
+
if watch
|
172
|
+
autorun(*marks)
|
173
|
+
else
|
174
|
+
monorun(*marks)
|
175
|
+
end
|
176
|
+
end
|
177
|
+
|
178
|
+
private
|
179
|
+
|
180
|
+
# Run rules once.
|
181
|
+
#
|
182
|
+
# Returns nothing.
|
183
|
+
def monorun(*marks)
|
184
|
+
Dir.chdir(root) do
|
185
|
+
fresh_digest(*marks) if fresh?
|
186
|
+
|
187
|
+
if marks.size > 0
|
188
|
+
run_bookmarks(*marks)
|
189
|
+
else
|
190
|
+
run_rules
|
191
|
+
end
|
192
|
+
end
|
193
|
+
end
|
194
|
+
|
195
|
+
# Run rules periodically.
|
196
|
+
#
|
197
|
+
# Returns nothing.
|
198
|
+
def autorun(*marks)
|
199
|
+
Dir.chdir(root) do
|
200
|
+
fresh_digest(*marks) if fresh?
|
201
|
+
|
202
|
+
trap("INT") { puts "\nPutting out the fire!"; exit }
|
203
|
+
puts "Fire started! (pid #{Process.pid})"
|
204
|
+
|
205
|
+
if marks.size > 0
|
206
|
+
loop do
|
207
|
+
run_bookmarks(*marks)
|
208
|
+
sleep(watch)
|
209
|
+
end
|
210
|
+
else
|
211
|
+
loop do
|
212
|
+
run_rules
|
213
|
+
sleep(watch)
|
214
|
+
end
|
215
|
+
end
|
216
|
+
end
|
217
|
+
end
|
218
|
+
|
219
|
+
# Returns [Hash]
|
220
|
+
attr :digests
|
221
|
+
|
222
|
+
# Run all rules (expect private rules).
|
223
|
+
#
|
224
|
+
# Returns nothing.
|
225
|
+
def run_rules
|
226
|
+
system.rules.each do |rule|
|
227
|
+
case rule
|
228
|
+
when Book
|
229
|
+
book = rule
|
230
|
+
book.rules.each do |rule|
|
231
|
+
next if rule.private?
|
232
|
+
rule.apply(latest_digest(rule))
|
233
|
+
end
|
234
|
+
else
|
235
|
+
next if rule.private?
|
236
|
+
rule.apply(latest_digest(rule))
|
237
|
+
end
|
238
|
+
end
|
239
|
+
|
240
|
+
clear_digests
|
241
|
+
|
242
|
+
digest.save
|
243
|
+
end
|
244
|
+
|
245
|
+
# Run only those rules with a specific bookmark.
|
246
|
+
#
|
247
|
+
# marks - Bookmark names. [Array<String>].
|
248
|
+
#
|
249
|
+
# Returns nothing.
|
250
|
+
def run_bookmarks(*marks)
|
251
|
+
system.rules.each do |rule|
|
252
|
+
case rule
|
253
|
+
when Book
|
254
|
+
book = rule
|
255
|
+
book.rules.each do |rule|
|
256
|
+
next unless marks.any?{ |mark| rule.mark?(mark) }
|
257
|
+
rule.apply(latest_digest(rule))
|
258
|
+
end
|
259
|
+
else
|
260
|
+
next unless marks.any?{ |mark| rule.mark?(mark) }
|
261
|
+
rule.apply(latest_digest(rule))
|
262
|
+
end
|
263
|
+
end
|
264
|
+
|
265
|
+
save_digests(*marks)
|
266
|
+
end
|
267
|
+
|
268
|
+
# get digest by name, if it doesn't exit create a new one.
|
269
|
+
def digest(name=nil)
|
270
|
+
@digests[name] ||= Digest.new(:ignore=>ignore, :name=>name)
|
271
|
+
end
|
272
|
+
|
273
|
+
# Get the most recent digest for a given rule.
|
274
|
+
#
|
275
|
+
# Returns [Digest]
|
276
|
+
def latest_digest(rule)
|
277
|
+
name = Digest.latest(*rule.bookmarks)
|
278
|
+
digest(name)
|
279
|
+
end
|
280
|
+
|
281
|
+
# Start with a clean slate by remove the digest.
|
282
|
+
#
|
283
|
+
# Returns nothing.
|
284
|
+
def fresh_digest(*marks)
|
285
|
+
if marks.empty?
|
286
|
+
clear_digests
|
287
|
+
else
|
288
|
+
marks.each do |mark|
|
289
|
+
d = @digests.delete(mark)
|
290
|
+
d.remove if d
|
291
|
+
end
|
292
|
+
end
|
293
|
+
end
|
294
|
+
|
295
|
+
# Clear away all digests but the main digest.
|
296
|
+
#
|
297
|
+
# Returns nothing.
|
298
|
+
def clear_digests
|
299
|
+
Digest.clear_digests
|
300
|
+
@digests = {}
|
301
|
+
end
|
302
|
+
|
303
|
+
# Save digests for given bookmarks.
|
304
|
+
#
|
305
|
+
# Returns nothing.
|
306
|
+
def save_digests(*bookmarks)
|
307
|
+
bookmarks.each do |mark|
|
308
|
+
digest(mark).save
|
309
|
+
end
|
310
|
+
end
|
311
|
+
|
312
|
+
#
|
313
|
+
#def save_pid
|
314
|
+
# File.open('.ergo/pid', 'w') do |f|
|
315
|
+
# f << Process.pid.to_s
|
316
|
+
# end
|
317
|
+
#end
|
318
|
+
|
319
|
+
# Save file digest.
|
320
|
+
#
|
321
|
+
# Returns nothing.
|
322
|
+
#def save_digest(*marks)
|
323
|
+
# digest.save(*marks)
|
324
|
+
#end
|
325
|
+
|
326
|
+
# System runner.
|
327
|
+
#
|
328
|
+
# Returns [Runner]
|
329
|
+
#def runner
|
330
|
+
# Runner.new(system)
|
331
|
+
#end
|
332
|
+
|
333
|
+
# TODO: load configuration
|
334
|
+
#
|
335
|
+
#def rc?
|
336
|
+
# Dir.glob('{.c,c,C}onfig{.rb,}').first
|
337
|
+
#end
|
338
|
+
|
339
|
+
# Oh why is this still around? It's the original routine
|
340
|
+
# for running rules. It worked ass backward too. Checking
|
341
|
+
# states and then applying rules that were attached to those
|
342
|
+
# states.
|
343
|
+
#
|
344
|
+
#def run
|
345
|
+
# @states.each do |state|
|
346
|
+
# session = OpenStruct.new
|
347
|
+
# next unless state.active?(info)
|
348
|
+
# @rules.each do |rule|
|
349
|
+
# if md = rule.match?(state)
|
350
|
+
# if rule.arity == 0 or md == true
|
351
|
+
# rule.call(info)
|
352
|
+
# else
|
353
|
+
# rule.call(info,*md[1..-1])
|
354
|
+
# end
|
355
|
+
# end
|
356
|
+
# end
|
357
|
+
# end
|
358
|
+
#end
|
359
|
+
|
360
|
+
# TODO: support rc profiles
|
361
|
+
#if config = Ergo.rc_config
|
362
|
+
# config.each do |c|
|
363
|
+
# if c.arity == 0
|
364
|
+
# system.instance_eval(&c)
|
365
|
+
# else
|
366
|
+
# c.call(system)
|
367
|
+
# end
|
368
|
+
# end
|
369
|
+
#end
|
370
|
+
|
371
|
+
#
|
372
|
+
class RootError < RuntimeError
|
373
|
+
end
|
374
|
+
|
375
|
+
end
|
376
|
+
|
377
|
+
end
|
@@ -0,0 +1,79 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
# TODO: Borrow code from Detroit for ShellUtils and beef her up!
|
4
|
+
|
5
|
+
# File system utility methods.
|
6
|
+
#
|
7
|
+
module ShellUtils
|
8
|
+
# Shell out via system call.
|
9
|
+
#
|
10
|
+
# Arguments
|
11
|
+
# args - Argument vector. [Array]
|
12
|
+
#
|
13
|
+
# Returns success of shell invocation.
|
14
|
+
def sh(*args)
|
15
|
+
env = (Hash === args.last ? args.pop : {})
|
16
|
+
puts args.join(' ')
|
17
|
+
system(env, *args)
|
18
|
+
end
|
19
|
+
|
20
|
+
#
|
21
|
+
def directory?(path)
|
22
|
+
File.directory?(path)
|
23
|
+
end
|
24
|
+
|
25
|
+
#
|
26
|
+
# Synchronize a destination directory with a source directory.
|
27
|
+
#
|
28
|
+
# TODO: Augment FileUtils instead.
|
29
|
+
# TODO: Not every action needs to be verbose.
|
30
|
+
#
|
31
|
+
def sync(src, dst, options={})
|
32
|
+
src_files = Dir[File.join(src, '**', '*')].map{ |f| f.sub(src+'/', '') }
|
33
|
+
dst_files = Dir[File.join(dst, '**', '*')].map{ |f| f.sub(dst+'/', '') }
|
34
|
+
|
35
|
+
removal = dst_files - src_files
|
36
|
+
|
37
|
+
rm_dirs, rm_files = [], []
|
38
|
+
removal.each do |f|
|
39
|
+
path = File.join(dst, f)
|
40
|
+
if File.directory?(path)
|
41
|
+
rm_dirs << path
|
42
|
+
else
|
43
|
+
rm_files << path
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
rm_files.each { |f| rm(f) }
|
48
|
+
rm_dirs.each { |d| rmdir(d) }
|
49
|
+
|
50
|
+
src_files.each do |f|
|
51
|
+
src_path = File.join(src, f)
|
52
|
+
dst_path = File.join(dst, f)
|
53
|
+
if File.directory?(src_path)
|
54
|
+
mkdir_p(dst_path)
|
55
|
+
else
|
56
|
+
parent = File.dirname(dst_path)
|
57
|
+
mkdir_p(parent) unless File.directory?(parent)
|
58
|
+
install(src_path, dst_path)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
|
63
|
+
#
|
64
|
+
# If FileUtils responds to a missing method, then call it.
|
65
|
+
#
|
66
|
+
def method_missing(s, *a, &b)
|
67
|
+
if FileUtils.respond_to?(s)
|
68
|
+
if $DRYRUN
|
69
|
+
FileUtils::DryRun.__send__(s, *a, &b)
|
70
|
+
else
|
71
|
+
FileUtils::Verbose.__send__(s, *a, &b)
|
72
|
+
end
|
73
|
+
else
|
74
|
+
super(s, *a, &b)
|
75
|
+
end
|
76
|
+
end
|
77
|
+
end
|
78
|
+
|
79
|
+
end
|