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
@@ -0,0 +1,58 @@
|
|
1
|
+
# We are going to try doing it this way. If i proves a problem
|
2
|
+
# we'll make a special class.
|
3
|
+
#
|
4
|
+
class TrueClass
|
5
|
+
def empty?
|
6
|
+
false
|
7
|
+
end
|
8
|
+
|
9
|
+
def &(other)
|
10
|
+
other
|
11
|
+
end
|
12
|
+
|
13
|
+
def |(other)
|
14
|
+
other
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
=begin
|
19
|
+
class TrueArray < Array
|
20
|
+
|
21
|
+
def each; end
|
22
|
+
|
23
|
+
def size
|
24
|
+
1 # ?
|
25
|
+
end
|
26
|
+
|
27
|
+
def empty?
|
28
|
+
false
|
29
|
+
end
|
30
|
+
|
31
|
+
def &(other)
|
32
|
+
other.dup
|
33
|
+
end
|
34
|
+
|
35
|
+
def |(other)
|
36
|
+
other.dup
|
37
|
+
end
|
38
|
+
|
39
|
+
## If this would have worked we would not have had
|
40
|
+
## to override Array.
|
41
|
+
#def coerce(other)
|
42
|
+
# return self, other
|
43
|
+
#end
|
44
|
+
end
|
45
|
+
=end
|
46
|
+
|
47
|
+
class Array
|
48
|
+
alias and_without_t :&
|
49
|
+
alias or_without_t :|
|
50
|
+
|
51
|
+
def |(other)
|
52
|
+
TrueClass === other ? dup : or_without_t(other)
|
53
|
+
end
|
54
|
+
|
55
|
+
def &(other)
|
56
|
+
TrueClass === other ? dup : and_without_t(other)
|
57
|
+
end
|
58
|
+
end
|
data/lib/ergo/digest.rb
ADDED
@@ -0,0 +1,196 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
##
|
4
|
+
# Digest class is used to read and write lists of files with their
|
5
|
+
# associated checksums. This class uses SHA1.
|
6
|
+
#
|
7
|
+
class Digest
|
8
|
+
|
9
|
+
# The name of the master digest.
|
10
|
+
MASTER_NAME = 'Master'
|
11
|
+
|
12
|
+
# The digest file to use if the root directory has a `log/` directory.
|
13
|
+
DIRECTORY = ".ergo/digest"
|
14
|
+
|
15
|
+
# Get the name of the most recent digest given a selection of names
|
16
|
+
# from which to choose.
|
17
|
+
#
|
18
|
+
# names - Selection of names. [Array<String>]
|
19
|
+
#
|
20
|
+
# Returns the digests name. [String]
|
21
|
+
def self.latest(*names)
|
22
|
+
names = names.select do |name|
|
23
|
+
File.exist?(File.join(DIRECTORY, "#{name}.digest"))
|
24
|
+
end
|
25
|
+
names.max do |name|
|
26
|
+
File.mtime(File.join(DIRECTORY, "#{name}.digest"))
|
27
|
+
end
|
28
|
+
end
|
29
|
+
|
30
|
+
# Remove all digests.
|
31
|
+
def self.clear_digests
|
32
|
+
Dir.glob(File.join(DIRECTORY, "*.digest")).each do |file|
|
33
|
+
FileUtils.rm(file)
|
34
|
+
end
|
35
|
+
end
|
36
|
+
|
37
|
+
# Remove digest by name.
|
38
|
+
def self.remove_digest(name)
|
39
|
+
file = File.join(DIRECTORY, "#{name}.digest")
|
40
|
+
if file.exist?(file)
|
41
|
+
FileUtils.rm(file)
|
42
|
+
end
|
43
|
+
end
|
44
|
+
|
45
|
+
# Instance of Ignore is used to filter "boring files".
|
46
|
+
#
|
47
|
+
# Returns [Ignore]
|
48
|
+
attr :ignore
|
49
|
+
|
50
|
+
# Name of digest, which corresponds to the rule bookmark.
|
51
|
+
#
|
52
|
+
# Returns [Ignore]
|
53
|
+
attr :name
|
54
|
+
|
55
|
+
# Set of files as they appear on disk.
|
56
|
+
attr :current
|
57
|
+
|
58
|
+
# Set of files as saved in the digest.
|
59
|
+
attr :saved
|
60
|
+
|
61
|
+
# Initialize new instance of Digest.
|
62
|
+
#
|
63
|
+
# Options
|
64
|
+
# ignore - Instance of Ignore for filtering unwanted files. [Ignore]
|
65
|
+
# mark - Name of digest to load. [String]
|
66
|
+
#
|
67
|
+
def initialize(options={})
|
68
|
+
@ignore = options[:ignore]
|
69
|
+
@name = options[:name] || MASTER_NAME
|
70
|
+
|
71
|
+
@current = {}
|
72
|
+
@saved = {}
|
73
|
+
|
74
|
+
read
|
75
|
+
refresh
|
76
|
+
end
|
77
|
+
|
78
|
+
# The digest file's path.
|
79
|
+
#
|
80
|
+
# Returns [String]
|
81
|
+
def filename
|
82
|
+
File.join(DIRECTORY, "#{name}.digest")
|
83
|
+
end
|
84
|
+
|
85
|
+
# Load digest from file system.
|
86
|
+
#
|
87
|
+
# Returns nothing.
|
88
|
+
def read
|
89
|
+
file = filename
|
90
|
+
|
91
|
+
# if the digest doesn't exist fallback to master digest
|
92
|
+
unless File.exist?(file)
|
93
|
+
file = File.join(DIRECTORY, "#{MASTER_NAME}.digest")
|
94
|
+
end
|
95
|
+
|
96
|
+
return unless File.exist?(file)
|
97
|
+
|
98
|
+
File.read(file).lines.each do |line|
|
99
|
+
if md = /^(\w+)\s+(.*?)$/.match(line)
|
100
|
+
@saved[md[2]] = md[1]
|
101
|
+
end
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
=begin
|
106
|
+
# Gather current digest for all files.
|
107
|
+
#
|
108
|
+
# Returns nothing.
|
109
|
+
def refresh
|
110
|
+
Dir['**/*'].each do |path|
|
111
|
+
if File.directory?(path)
|
112
|
+
# how to handle directories as a whole?
|
113
|
+
elsif File.exist?(path)
|
114
|
+
id = checksum(path)
|
115
|
+
@current[path] = id
|
116
|
+
end
|
117
|
+
end
|
118
|
+
end
|
119
|
+
=end
|
120
|
+
|
121
|
+
# Gather current digest for all files.
|
122
|
+
#
|
123
|
+
# Returns nothing.
|
124
|
+
def refresh
|
125
|
+
list = Dir['**/*']
|
126
|
+
list = filter(list)
|
127
|
+
list.each do |path|
|
128
|
+
if File.directory?(path)
|
129
|
+
# how to handle directories as a whole?
|
130
|
+
elsif File.exist?(path)
|
131
|
+
id = checksum(path)
|
132
|
+
@current[path] = id
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Save current digest.
|
138
|
+
#
|
139
|
+
# Returns nothing.
|
140
|
+
def save
|
141
|
+
FileUtils.mkdir_p(DIRECTORY) unless File.directory?(DIRECTORY)
|
142
|
+
File.open(filename, 'w') do |f|
|
143
|
+
f << to_s
|
144
|
+
end
|
145
|
+
end
|
146
|
+
|
147
|
+
# Remove digest.
|
148
|
+
def remove
|
149
|
+
if File.exist?(filename)
|
150
|
+
FileUtils.rm(filename)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
# Produce the test representation of the digest that is stored to disk.
|
155
|
+
#
|
156
|
+
# Returns digest file format. [String]
|
157
|
+
def to_s
|
158
|
+
s = ""
|
159
|
+
current.each do |path, id|
|
160
|
+
s << "#{id} #{path}\n"
|
161
|
+
end
|
162
|
+
s
|
163
|
+
end
|
164
|
+
|
165
|
+
# Compute the sha1 identifer for a file.
|
166
|
+
#
|
167
|
+
# file - path to a file
|
168
|
+
#
|
169
|
+
# Returns [String] SHA1 digest string.
|
170
|
+
def checksum(file)
|
171
|
+
sha = ::Digest::SHA1.new
|
172
|
+
File.open(file, 'r') do |fh|
|
173
|
+
fh.each_line do |l|
|
174
|
+
sha << l
|
175
|
+
end
|
176
|
+
end
|
177
|
+
sha.hexdigest
|
178
|
+
end
|
179
|
+
|
180
|
+
# Filter files of those to be ignored.
|
181
|
+
#
|
182
|
+
# Return [Array<String>]
|
183
|
+
def filter(list)
|
184
|
+
case ignore
|
185
|
+
when Ignore
|
186
|
+
ignore.filter(list)
|
187
|
+
when Array
|
188
|
+
list.reject{ |path| ignore.any?{ |ig| /^#{ig}/ =~ path } }
|
189
|
+
else
|
190
|
+
list
|
191
|
+
end
|
192
|
+
end
|
193
|
+
|
194
|
+
end
|
195
|
+
|
196
|
+
end
|
data/lib/ergo/ignore.rb
ADDED
@@ -0,0 +1,146 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
# This file can be used as an alternative to using the #ignore method
|
4
|
+
# to define what paths to ignore.
|
5
|
+
IGNORE_FILE = '.ergo/ignore'
|
6
|
+
|
7
|
+
##
|
8
|
+
# Encapsulates list of file globs to be ignored.
|
9
|
+
#
|
10
|
+
class Ignore
|
11
|
+
include Enumerable
|
12
|
+
|
13
|
+
# Initialize new instance of Ignore.
|
14
|
+
#
|
15
|
+
# Returns nothing.
|
16
|
+
def initialize(options={})
|
17
|
+
@file = options[:file]
|
18
|
+
@root = options[:root]
|
19
|
+
|
20
|
+
@ignore = load_ignore
|
21
|
+
end
|
22
|
+
|
23
|
+
# Filter a list of files in accordance with the
|
24
|
+
# ignore list.
|
25
|
+
#
|
26
|
+
# files - The list of files. [Array<String>]
|
27
|
+
#
|
28
|
+
# Returns [Array<String>]
|
29
|
+
def filter(files)
|
30
|
+
list = []
|
31
|
+
files.each do |file|
|
32
|
+
hit = any? do |pattern|
|
33
|
+
match?(pattern, file)
|
34
|
+
end
|
35
|
+
list << file unless hit
|
36
|
+
end
|
37
|
+
list
|
38
|
+
end
|
39
|
+
|
40
|
+
# Returns [Array<String>]
|
41
|
+
#def ignore
|
42
|
+
# @ignore ||= load_ignore
|
43
|
+
#end
|
44
|
+
|
45
|
+
# Ignore file.
|
46
|
+
def file
|
47
|
+
@file ||= (
|
48
|
+
Dir["{.gitignore,.hgignore,#{IGNORE_FILE}}"].first
|
49
|
+
)
|
50
|
+
end
|
51
|
+
|
52
|
+
#
|
53
|
+
def each
|
54
|
+
to_a.each{ |g| yield g }
|
55
|
+
end
|
56
|
+
|
57
|
+
#
|
58
|
+
def size
|
59
|
+
to_a.size
|
60
|
+
end
|
61
|
+
|
62
|
+
#
|
63
|
+
def to_a
|
64
|
+
@ignore #||= load_ignore
|
65
|
+
end
|
66
|
+
|
67
|
+
#
|
68
|
+
def replace(*globs)
|
69
|
+
@ignore = globs.flatten
|
70
|
+
end
|
71
|
+
|
72
|
+
#
|
73
|
+
def concat(*globs)
|
74
|
+
@ignore.concat(globs.flatten)
|
75
|
+
end
|
76
|
+
|
77
|
+
#private
|
78
|
+
|
79
|
+
#def all_ignored_files
|
80
|
+
# list = []
|
81
|
+
# ignore.each do |glob|
|
82
|
+
# if glob.start_with?('/')
|
83
|
+
# list.concat Dir[File.join(@root, glob)]
|
84
|
+
# else
|
85
|
+
# list.concat Dir[File.join(@root, '**', glob)]
|
86
|
+
# end
|
87
|
+
# end
|
88
|
+
# list
|
89
|
+
#end
|
90
|
+
|
91
|
+
# Load ignore file. Removes blank lines and line starting with `#`.
|
92
|
+
#
|
93
|
+
# Returns [Array<String>]
|
94
|
+
def load_ignore
|
95
|
+
f = file
|
96
|
+
i = []
|
97
|
+
if f && File.exist?(f)
|
98
|
+
File.read(f).lines.each do |line|
|
99
|
+
glob = line.strip
|
100
|
+
next if glob.empty?
|
101
|
+
next if glob.start_with?('#')
|
102
|
+
i << glob
|
103
|
+
end
|
104
|
+
end
|
105
|
+
i
|
106
|
+
end
|
107
|
+
|
108
|
+
# Given a pattern and a file, does the file match the
|
109
|
+
# pattern? This code is based on the rules used by
|
110
|
+
# git's .gitignore file.
|
111
|
+
#
|
112
|
+
# TODO: The code is probably not quite right.
|
113
|
+
#
|
114
|
+
# Returns [Boolean]
|
115
|
+
def match?(pattern, file)
|
116
|
+
if pattern.start_with?('!')
|
117
|
+
return !match?(pattern.sub('!','').strip)
|
118
|
+
end
|
119
|
+
|
120
|
+
dir = pattern.end_with?('/')
|
121
|
+
pattern = pattern.chomp('/') if dir
|
122
|
+
|
123
|
+
if pattern.start_with?('/')
|
124
|
+
fnmatch?(pattern.sub('/',''), file)
|
125
|
+
else
|
126
|
+
if dir
|
127
|
+
fnmatch?(File.join(pattern, '**', '*'), file) ||
|
128
|
+
fnmatch?(pattern, file) && File.directory?(file)
|
129
|
+
elsif pattern.include?('/')
|
130
|
+
fnmatch?(pattern, file)
|
131
|
+
else
|
132
|
+
fnmatch?(File.join('**',pattern), file)
|
133
|
+
end
|
134
|
+
end
|
135
|
+
end
|
136
|
+
|
137
|
+
# Shortcut to `File.fnmatch?` method.
|
138
|
+
#
|
139
|
+
# Returns [Boolean]
|
140
|
+
def fnmatch?(pattern, file, mode=File::FNM_PATHNAME)
|
141
|
+
File.fnmatch?(pattern, file, File::FNM_PATHNAME)
|
142
|
+
end
|
143
|
+
|
144
|
+
end
|
145
|
+
|
146
|
+
end
|
data/lib/ergo/match.rb
ADDED
@@ -0,0 +1,26 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
# Match is a subclass of a string that also stores the
|
4
|
+
# MatchData then matched against it in a Regexp comparison.
|
5
|
+
#
|
6
|
+
class Match < String
|
7
|
+
# Initialize a new instance of Match.
|
8
|
+
#
|
9
|
+
# string - The string. [String]
|
10
|
+
# matchdata - The match data. [MatchData]
|
11
|
+
#
|
12
|
+
def intialize(string, matchdata)
|
13
|
+
replace(string)
|
14
|
+
@matchdata = matchdata
|
15
|
+
end
|
16
|
+
|
17
|
+
# The match data that resulted from
|
18
|
+
# a successful Regexp against the string.
|
19
|
+
#
|
20
|
+
# Returns [MatchData]
|
21
|
+
def matchdata
|
22
|
+
@matchdata
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
end
|
data/lib/ergo/rule.rb
ADDED
@@ -0,0 +1,134 @@
|
|
1
|
+
module Ergo
|
2
|
+
|
3
|
+
# Rule class encapsulates a *rule* definition.
|
4
|
+
#
|
5
|
+
class Rule
|
6
|
+
# Initialize new instanance of Rule.
|
7
|
+
#
|
8
|
+
# state - State condition. [Logic]
|
9
|
+
# procedure - Procedure to run if logic condition is met. [Proc]
|
10
|
+
#
|
11
|
+
# Options
|
12
|
+
# desc - Description of rule. [String]
|
13
|
+
# mark - List of bookmark names. [Array<String>]
|
14
|
+
#
|
15
|
+
def initialize(state, options={}, &procedure)
|
16
|
+
self.state = state
|
17
|
+
self.desc = options[:desc] || options[:description]
|
18
|
+
self.mark = options[:mark] || options[:bookmarks]
|
19
|
+
self.private = options[:private]
|
20
|
+
|
21
|
+
@proc = procedure
|
22
|
+
end
|
23
|
+
|
24
|
+
# Access logic condition.
|
25
|
+
#
|
26
|
+
# Returns [State]
|
27
|
+
attr :state
|
28
|
+
|
29
|
+
# Description of rule.
|
30
|
+
#
|
31
|
+
# Returns [String]
|
32
|
+
def description
|
33
|
+
@description
|
34
|
+
end
|
35
|
+
|
36
|
+
# Returns the description.
|
37
|
+
#
|
38
|
+
# Returns [String]
|
39
|
+
alias :to_s :description
|
40
|
+
|
41
|
+
# Rule bookmarks.
|
42
|
+
#
|
43
|
+
# Returns [Array<String>]
|
44
|
+
def bookmarks
|
45
|
+
@bookmarks
|
46
|
+
end
|
47
|
+
|
48
|
+
#
|
49
|
+
def bookmark?(name)
|
50
|
+
@bookmarks.include?(name.to_s)
|
51
|
+
end
|
52
|
+
alias :mark? :bookmark?
|
53
|
+
|
54
|
+
# Is the rule private? A private rule does not run with the "master book",
|
55
|
+
# only when it's specific book is invoked.
|
56
|
+
def private?
|
57
|
+
@private
|
58
|
+
end
|
59
|
+
|
60
|
+
# Rule procedure.
|
61
|
+
#
|
62
|
+
# Returns [Proc]
|
63
|
+
def to_proc
|
64
|
+
@proc
|
65
|
+
end
|
66
|
+
|
67
|
+
# Apply rule, running the rule's procedure if the state
|
68
|
+
# condition is satisfied.
|
69
|
+
#
|
70
|
+
# Returns nothing.
|
71
|
+
def apply(digest)
|
72
|
+
case state
|
73
|
+
when true
|
74
|
+
call
|
75
|
+
when false, nil
|
76
|
+
else
|
77
|
+
result_set = state.call(digest)
|
78
|
+
if result_set && !result_set.empty?
|
79
|
+
call(result_set)
|
80
|
+
end
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
# Alias for #apply.
|
85
|
+
alias :invoke :apply
|
86
|
+
|
87
|
+
# Convenience method for producing a rule list.
|
88
|
+
#
|
89
|
+
# Rertuns [Array]
|
90
|
+
def to_a
|
91
|
+
[description, bookmarks, private?]
|
92
|
+
end
|
93
|
+
|
94
|
+
protected
|
95
|
+
|
96
|
+
# Set state of rule.
|
97
|
+
def state=(state)
|
98
|
+
#raise unless State === state || Boolean === state
|
99
|
+
@state = state
|
100
|
+
end
|
101
|
+
|
102
|
+
# Set bookmark(s) of rule.
|
103
|
+
def mark=(names)
|
104
|
+
@bookmarks = Array(names).map{ |b| b.to_s }
|
105
|
+
end
|
106
|
+
|
107
|
+
# Set privacy of rule. A private rule does not run with the "master book",
|
108
|
+
# only when it's specific book is invoked.
|
109
|
+
def private=(boolean)
|
110
|
+
@private = !! boolean
|
111
|
+
end
|
112
|
+
|
113
|
+
# Set description of rule.
|
114
|
+
def desc=(string)
|
115
|
+
@description = string.to_s
|
116
|
+
end
|
117
|
+
|
118
|
+
# Run rule procedure.
|
119
|
+
#
|
120
|
+
# result_set - The result set returned by the logic condition.
|
121
|
+
#
|
122
|
+
# Returns whatever the procedure returns. [Object]
|
123
|
+
def call(*result_set)
|
124
|
+
if @proc.arity == 0
|
125
|
+
@proc.call
|
126
|
+
else
|
127
|
+
#@procedure.call(session, *args)
|
128
|
+
@proc.call(*result_set)
|
129
|
+
end
|
130
|
+
end
|
131
|
+
|
132
|
+
end
|
133
|
+
|
134
|
+
end
|