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