melon 0.2.0 → 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/History.txt +4 -0
- data/features/add.feature +6 -1
- data/lib/melon/cli.rb +13 -4
- data/lib/melon/commands/add.rb +56 -0
- data/lib/melon/commands/base.rb +73 -0
- data/lib/melon/commands/check.rb +35 -0
- data/lib/melon/commands/help.rb +23 -0
- data/lib/melon/commands/list.rb +33 -0
- data/lib/melon/commands/show.rb +36 -0
- data/lib/melon/commands.rb +16 -213
- data/lib/melon/hasher.rb +1 -0
- data/lib/melon/helpers.rb +16 -2
- data/lib/melon/version.rb +1 -1
- metadata +10 -4
data/History.txt
CHANGED
data/features/add.feature
CHANGED
@@ -51,7 +51,12 @@ Feature: Adding files to the database
|
|
51
51
|
"""
|
52
52
|
Third test file
|
53
53
|
"""
|
54
|
-
|
54
|
+
And a file named "test_file" with:
|
55
|
+
"""
|
56
|
+
Test file that's not in the subfolder
|
57
|
+
"""
|
58
|
+
When I run "melon -d test.db add -r dir test_file"
|
55
59
|
Then the output should contain "dir/test1"
|
56
60
|
And the output should contain "dir/test2"
|
57
61
|
And the output should contain "dir/test/test3"
|
62
|
+
And the output should contain "test_file"
|
data/lib/melon/cli.rb
CHANGED
@@ -57,6 +57,8 @@ module Melon
|
|
57
57
|
def parse_options
|
58
58
|
options = self.class.default_options
|
59
59
|
|
60
|
+
# TODO: empty args should be -h
|
61
|
+
|
60
62
|
parser = OptionParser.new do |p|
|
61
63
|
p.banner = "Usage: melon [options] COMMAND [command-options] [ARGS]"
|
62
64
|
|
@@ -64,11 +66,18 @@ module Melon
|
|
64
66
|
p.separator "Commands:"
|
65
67
|
p.separator ""
|
66
68
|
|
67
|
-
|
68
|
-
|
69
|
-
|
70
|
-
|
69
|
+
Commands.each do |command|
|
70
|
+
# help goes last
|
71
|
+
if command.command_name == 'help'
|
72
|
+
next
|
73
|
+
end
|
71
74
|
|
75
|
+
p.separator format_command(command.command_name,
|
76
|
+
command.description)
|
77
|
+
end
|
78
|
+
# TODO: add help command back into parser helptext
|
79
|
+
# p.separator format_command(Commands::Help.command_name,
|
80
|
+
# Commands::Help.description)
|
72
81
|
p.separator ""
|
73
82
|
p.separator "Options:"
|
74
83
|
p.separator ""
|
@@ -0,0 +1,56 @@
|
|
1
|
+
require 'melon/hasher'
|
2
|
+
require 'melon/commands/base'
|
3
|
+
|
4
|
+
module Melon
|
5
|
+
module Commands
|
6
|
+
class Add < Base
|
7
|
+
def self.description
|
8
|
+
"Add files to the melon database"
|
9
|
+
end
|
10
|
+
|
11
|
+
def parser_options(parser)
|
12
|
+
parser.on("-q", "--quiet", "Suppress printing of hash and path") do
|
13
|
+
options.quiet = true
|
14
|
+
end
|
15
|
+
|
16
|
+
parser.on("-r", "--recursive", "Recursively add directory contents") do
|
17
|
+
options.recursive = true
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
parse_options!
|
23
|
+
|
24
|
+
if options.recursive
|
25
|
+
self.args = recursively_expand(args)
|
26
|
+
end
|
27
|
+
|
28
|
+
options.database.transaction do
|
29
|
+
args.each do |arg|
|
30
|
+
filename = File.expand_path(arg)
|
31
|
+
|
32
|
+
if File.directory?(filename)
|
33
|
+
error "argument is a directory: #{arg}"
|
34
|
+
end
|
35
|
+
|
36
|
+
if options.database[:by_path][filename]
|
37
|
+
error "path already present in database: #{arg}"
|
38
|
+
end
|
39
|
+
|
40
|
+
# hash strategy should be encapsulated, ergo indirection here
|
41
|
+
hash = Hasher.digest(filename)
|
42
|
+
|
43
|
+
if options.database[:by_hash][hash]
|
44
|
+
error "file exists elsewhere in the database: #{arg}"
|
45
|
+
end
|
46
|
+
|
47
|
+
|
48
|
+
options.database[:by_hash][hash] = filename
|
49
|
+
options.database[:by_path][filename] = hash
|
50
|
+
puts "#{hash}:#{filename}" unless options.quiet
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|
@@ -0,0 +1,73 @@
|
|
1
|
+
require 'optparse'
|
2
|
+
require 'melon/helpers'
|
3
|
+
|
4
|
+
module Melon
|
5
|
+
module Commands
|
6
|
+
class Base
|
7
|
+
include Helpers
|
8
|
+
attr_accessor :args, :options
|
9
|
+
attr_reader :description
|
10
|
+
|
11
|
+
def initialize(args, options)
|
12
|
+
self.args = args
|
13
|
+
self.options = options
|
14
|
+
end
|
15
|
+
|
16
|
+
def self.command_name
|
17
|
+
self.to_s.split("::")[-1].downcase
|
18
|
+
end
|
19
|
+
|
20
|
+
def parser
|
21
|
+
@parser ||= OptionParser.new do |p|
|
22
|
+
p.banner = usagebanner
|
23
|
+
p.separator ""
|
24
|
+
p.separator blockquote(self.class.description + ".", :margin => 0)
|
25
|
+
p.separator ""
|
26
|
+
|
27
|
+
if helptext
|
28
|
+
p.separator blockquote(" " + helptext)
|
29
|
+
p.separator ""
|
30
|
+
end
|
31
|
+
|
32
|
+
if self.respond_to? :parser_options
|
33
|
+
p.separator "Options:"
|
34
|
+
p.separator ""
|
35
|
+
parser_options(p)
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
|
40
|
+
|
41
|
+
def usagebanner
|
42
|
+
usage = "Usage: melon #{self.class.command_name}"
|
43
|
+
usage << " [options]" if self.respond_to?(:parser_options)
|
44
|
+
usage << ' ' << usageargs if usageargs
|
45
|
+
usage
|
46
|
+
end
|
47
|
+
|
48
|
+
def usageargs; end
|
49
|
+
def helptext; end
|
50
|
+
|
51
|
+
def self.description
|
52
|
+
raise
|
53
|
+
end
|
54
|
+
|
55
|
+
def parse_options!
|
56
|
+
begin
|
57
|
+
parser.parse!(args)
|
58
|
+
rescue OptionParser::ParseError => e
|
59
|
+
error "#{self.class.command_name}: #{e}"
|
60
|
+
end
|
61
|
+
|
62
|
+
# verify remaining args are files - overrideable
|
63
|
+
verify_args
|
64
|
+
end
|
65
|
+
|
66
|
+
def verify_args
|
67
|
+
args.each do |arg|
|
68
|
+
error "no such file: #{arg}" unless File.exists?(arg)
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
73
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
require 'melon/commands/base'
|
2
|
+
|
3
|
+
module Melon
|
4
|
+
module Commands
|
5
|
+
class Check < Base
|
6
|
+
def self.description
|
7
|
+
"Determine whether or not a copy of a file resides in the database"
|
8
|
+
end
|
9
|
+
|
10
|
+
def helptext
|
11
|
+
<<EOS
|
12
|
+
If the file's hash matches a hash in the database, nothing is
|
13
|
+
printed. Otherwise, the full path to the file is printed.
|
14
|
+
EOS
|
15
|
+
end
|
16
|
+
|
17
|
+
def usageargs
|
18
|
+
"file [file [file ...]"
|
19
|
+
end
|
20
|
+
|
21
|
+
def run
|
22
|
+
parse_options!
|
23
|
+
|
24
|
+
options.database.transaction do
|
25
|
+
args.each do |filename|
|
26
|
+
hash = Hasher.digest(filename)
|
27
|
+
unless options.database[:by_hash][hash]
|
28
|
+
puts File.expand_path(filename)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,23 @@
|
|
1
|
+
require 'melon/commands/base'
|
2
|
+
require 'melon/commands'
|
3
|
+
|
4
|
+
module Melon
|
5
|
+
module Commands
|
6
|
+
class Help < Base
|
7
|
+
def self.description
|
8
|
+
"Get help with a specific command"
|
9
|
+
end
|
10
|
+
|
11
|
+
def run
|
12
|
+
help = args.shift
|
13
|
+
begin
|
14
|
+
puts Commands[help.capitalize].new(args, options).parser
|
15
|
+
rescue NameError => e
|
16
|
+
# don't swallow NoMethodErrors
|
17
|
+
raise e unless e.instance_of?(NameError)
|
18
|
+
error "unrecognized command: #{help}"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
@@ -0,0 +1,33 @@
|
|
1
|
+
require 'melon/commands/base'
|
2
|
+
|
3
|
+
module Melon
|
4
|
+
module Commands
|
5
|
+
class List < Base
|
6
|
+
def self.description
|
7
|
+
"List the files tracked by the database"
|
8
|
+
end
|
9
|
+
|
10
|
+
def parser_options(parser)
|
11
|
+
parser.on("-p", "--paths", "print only paths") do
|
12
|
+
options.only_paths = true
|
13
|
+
end
|
14
|
+
# TODO: re-enable this as -h after help command goes in
|
15
|
+
# parser.on("--hashes", "print only hashes") { options.only_hashes = true }
|
16
|
+
end
|
17
|
+
|
18
|
+
def verify_args
|
19
|
+
error "invalid argument: #{args.shift}" unless args.empty?
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
parse_options!
|
24
|
+
|
25
|
+
options.database.transaction do
|
26
|
+
options.database[:by_hash].each_pair do |hash, path|
|
27
|
+
puts "#{path}:#{hash}"
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
@@ -0,0 +1,36 @@
|
|
1
|
+
require 'melon/commands/base'
|
2
|
+
|
3
|
+
module Melon
|
4
|
+
module Commands
|
5
|
+
class Show < Base
|
6
|
+
def self.description
|
7
|
+
"Show where the database thinks a file is located"
|
8
|
+
end
|
9
|
+
|
10
|
+
def usageargs
|
11
|
+
"file [file [file ...]]"
|
12
|
+
end
|
13
|
+
|
14
|
+
def helptext
|
15
|
+
<<EOS
|
16
|
+
If the file's hash matches a hash in the database, then
|
17
|
+
the associated path in the database is printed. Otherwise,
|
18
|
+
nothing is printed.
|
19
|
+
EOS
|
20
|
+
end
|
21
|
+
|
22
|
+
def run
|
23
|
+
parse_options!
|
24
|
+
|
25
|
+
options.database.transaction do
|
26
|
+
args.each do |filename|
|
27
|
+
hash = Hasher.digest(filename)
|
28
|
+
if path = options.database[:by_hash][hash]
|
29
|
+
puts path
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
data/lib/melon/commands.rb
CHANGED
@@ -1,14 +1,13 @@
|
|
1
|
-
require
|
2
|
-
require 'melon/helpers'
|
1
|
+
Dir[File.join(File.dirname(__FILE__), "commands/*.rb")].each {|f| require f}
|
3
2
|
|
4
3
|
module Melon
|
5
4
|
module Commands
|
6
5
|
def self.each
|
7
6
|
consts = []
|
8
|
-
|
9
|
-
self.constants.each do |c|
|
7
|
+
self.constants.sort.each do |c|
|
10
8
|
const = self.const_get(c)
|
11
|
-
|
9
|
+
|
10
|
+
if const.superclass == Base
|
12
11
|
consts << const
|
13
12
|
yield const
|
14
13
|
end
|
@@ -20,215 +19,19 @@ module Melon
|
|
20
19
|
alias :[] :const_get
|
21
20
|
end
|
22
21
|
|
23
|
-
|
22
|
+
|
23
|
+
# 1.0 list
|
24
|
+
# TODO: needs a 'verify' command to check integrity of database
|
24
25
|
# both internal 2-hash consistency (consistency) and db<->filesystem
|
25
26
|
# matching up (integrity) [file exists, hashes match]
|
26
|
-
# needs a 'remove' command, or some way to deal with deletes/renames
|
27
|
-
#
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
|
34
|
-
|
35
|
-
self.options = options
|
36
|
-
end
|
37
|
-
|
38
|
-
def parser
|
39
|
-
raise
|
40
|
-
end
|
41
|
-
|
42
|
-
def self.description
|
43
|
-
raise
|
44
|
-
end
|
45
|
-
|
46
|
-
def parse_options!
|
47
|
-
begin
|
48
|
-
parser.parse!(args)
|
49
|
-
rescue OptionParser::ParseError => e
|
50
|
-
error "#{self.class.to_s.split("::").last.downcase}: #{e}"
|
51
|
-
end
|
52
|
-
|
53
|
-
# verify remaining args are files - overrideable
|
54
|
-
verify_args
|
55
|
-
end
|
56
|
-
|
57
|
-
def verify_args
|
58
|
-
args.each do |arg|
|
59
|
-
error "no such file: #{arg}" unless File.exists?(arg)
|
60
|
-
end
|
61
|
-
end
|
62
|
-
end
|
63
|
-
|
64
|
-
class Add < Base
|
65
|
-
|
66
|
-
def self.description
|
67
|
-
"Add files to the melon database"
|
68
|
-
end
|
69
|
-
|
70
|
-
def parser
|
71
|
-
@parser ||= OptionParser.new do |p|
|
72
|
-
p.banner = "Usage: melon add [options] file [file [file ...]]"
|
73
|
-
p.separator ""
|
74
|
-
p.separator blockquote(Add.description + ".")
|
75
|
-
|
76
|
-
p.separator ""
|
77
|
-
p.separator "Options:"
|
78
|
-
p.separator ""
|
79
|
-
|
80
|
-
p.on("-q", "--quiet", "Suppress printing of hash and path") do
|
81
|
-
options.quiet = true
|
82
|
-
end
|
83
|
-
|
84
|
-
p.on("-r", "--recursive", "Recursively add directory contents") do
|
85
|
-
options.recursive = true
|
86
|
-
end
|
87
|
-
|
88
|
-
# p.on("-f", "--force",
|
89
|
-
# "Force the recalculation of the path that",
|
90
|
-
# " already exists in the database") do
|
91
|
-
# options.force = true
|
92
|
-
# end
|
93
|
-
end
|
94
|
-
end
|
95
|
-
|
96
|
-
def run
|
97
|
-
parse_options!
|
98
|
-
|
99
|
-
if options.recursive
|
100
|
-
self.args = args.collect do |arg|
|
101
|
-
if File.directory?(arg)
|
102
|
-
Dir["#{arg}/**/*"]
|
103
|
-
else
|
104
|
-
arg
|
105
|
-
end
|
106
|
-
end.flatten.reject { |arg| File.directory?(arg) }
|
107
|
-
end
|
108
|
-
|
109
|
-
options.database.transaction do
|
110
|
-
args.each do |arg|
|
111
|
-
filename = File.expand_path(arg)
|
112
|
-
|
113
|
-
if File.directory?(filename)
|
114
|
-
error "argument is a directory: #{arg}"
|
115
|
-
end
|
116
|
-
|
117
|
-
if options.database[:by_path][filename]# and !options.force
|
118
|
-
error "path already present in database: #{arg}"
|
119
|
-
end
|
120
|
-
|
121
|
-
# hash strategy should be encapsulated, ergo indirection here
|
122
|
-
hash = Hasher.digest(filename)
|
123
|
-
|
124
|
-
if options.database[:by_hash][hash]
|
125
|
-
error "file exists elsewhere in the database: #{arg}"
|
126
|
-
end
|
127
|
-
|
128
|
-
|
129
|
-
options.database[:by_hash][hash] = filename
|
130
|
-
options.database[:by_path][filename] = hash
|
131
|
-
puts "#{hash}:#{filename}" unless options.quiet
|
132
|
-
end
|
133
|
-
end
|
134
|
-
end
|
135
|
-
end
|
136
|
-
|
137
|
-
class Show < Base
|
138
|
-
def self.description
|
139
|
-
"Show where the database thinks a file is located"
|
140
|
-
end
|
141
|
-
|
142
|
-
def parser
|
143
|
-
@parser ||= OptionParser.new do |p|
|
144
|
-
p.banner = "Usage: melon show file [file [file ...]]"
|
145
|
-
p.separator ""
|
146
|
-
p.separator blockquote(self.class.description + <<EOS
|
147
|
-
. If the file's hash matches a hash in the database, then
|
148
|
-
the associated path in the database is printed. Otherwise,
|
149
|
-
nothing is printed.
|
150
|
-
EOS
|
151
|
-
)
|
152
|
-
p.separator ""
|
153
|
-
|
154
|
-
end
|
155
|
-
end
|
156
|
-
|
157
|
-
def run
|
158
|
-
parse_options!
|
159
|
-
|
160
|
-
options.database.transaction do
|
161
|
-
args.each do |filename|
|
162
|
-
hash = Hasher.digest(filename)
|
163
|
-
if path = options.database[:by_hash][hash]
|
164
|
-
puts path
|
165
|
-
end
|
166
|
-
end
|
167
|
-
end
|
168
|
-
end
|
169
|
-
end
|
170
|
-
|
171
|
-
class Check < Base
|
172
|
-
def self.description
|
173
|
-
"Determine whether or not a copy of a file resides in the database"
|
174
|
-
end
|
175
|
-
|
176
|
-
def parser
|
177
|
-
@parser ||= OptionParser.new do |p|
|
178
|
-
p.banner = "Usage: melon check file [file [file ...]]"
|
179
|
-
p.separator ""
|
180
|
-
p.separator blockquote(self.class.description + <<EOS
|
181
|
-
. If the file's hash matches a hash in the database, nothing is
|
182
|
-
printed. Otherwise, the full path to the file is printed.
|
183
|
-
EOS
|
184
|
-
)
|
185
|
-
p.separator ""
|
186
|
-
|
187
|
-
end
|
188
|
-
end
|
189
|
-
|
190
|
-
def run
|
191
|
-
parse_options!
|
192
|
-
|
193
|
-
options.database.transaction do
|
194
|
-
args.each do |filename|
|
195
|
-
hash = Hasher.digest(filename)
|
196
|
-
unless options.database[:by_hash][hash]
|
197
|
-
puts File.expand_path(filename)
|
198
|
-
end
|
199
|
-
end
|
200
|
-
end
|
201
|
-
end
|
202
|
-
end
|
203
|
-
|
204
|
-
class List < Base
|
205
|
-
def self.description
|
206
|
-
"List the files tracked by the database"
|
207
|
-
end
|
208
|
-
|
209
|
-
def parser
|
210
|
-
@parser ||= OptionParser.new do |p|
|
211
|
-
p.banner = "Usage: melon list"
|
212
|
-
p.separator ""
|
213
|
-
p.separator blockquote(self.class.description)
|
214
|
-
p.separator ""
|
215
|
-
|
216
|
-
end
|
217
|
-
end
|
218
|
-
|
219
|
-
def verify_args
|
220
|
-
error "invalid argument: #{args.shift}" unless args.empty?
|
221
|
-
end
|
222
|
-
|
223
|
-
def run
|
224
|
-
parse_options!
|
225
|
-
|
226
|
-
options.database.transaction do
|
227
|
-
options.database[:by_hash].each_pair do |hash, path|
|
228
|
-
puts "#{path}:#{hash}"
|
229
|
-
end
|
230
|
-
end
|
231
|
-
end
|
232
|
-
end
|
27
|
+
# TODO: needs a 'remove' command, or some way to deal with deletes/renames
|
28
|
+
# remove: given a tracked file, removes it
|
29
|
+
# given an untracked file, it hashes it
|
30
|
+
# and removes it by hash
|
31
|
+
# TODO: list needs --paths(only) and --hashes(only)
|
32
|
+
# --count
|
33
|
+
# TODO: check needs -r
|
34
|
+
# TODO: update- a function of add, ignore files that are already present in the db
|
35
|
+
# TODO: handle moving a file somehow -- hopefully a function of update
|
233
36
|
end
|
234
37
|
end
|
data/lib/melon/hasher.rb
CHANGED
data/lib/melon/helpers.rb
CHANGED
@@ -6,7 +6,7 @@ module Melon
|
|
6
6
|
:width => 22,
|
7
7
|
:wrap => 80
|
8
8
|
}.update(options)
|
9
|
-
|
9
|
+
|
10
10
|
pad = "\n" + ' ' * options[:width]
|
11
11
|
desc = self.wrap_text(desc, options[:wrap] - options[:width])
|
12
12
|
desc = desc.split("\n").join(pad)
|
@@ -25,8 +25,12 @@ module Melon
|
|
25
25
|
:margin => 4,
|
26
26
|
:wrap => 70
|
27
27
|
}.update(options)
|
28
|
+
|
28
29
|
options[:width] = options.delete(:margin)
|
29
|
-
|
30
|
+
options[:margin] = 0
|
31
|
+
format_command('', string.gsub(/\s+/,' ').
|
32
|
+
gsub(/\. /, '. ').
|
33
|
+
gsub(/^ /, ' '), options)
|
30
34
|
end
|
31
35
|
|
32
36
|
def error(error_obj_or_str, code = 1)
|
@@ -39,5 +43,15 @@ module Melon
|
|
39
43
|
$stderr.puts "melon: #{error_str}"
|
40
44
|
exit code
|
41
45
|
end
|
46
|
+
|
47
|
+
def recursively_expand(filelist)
|
48
|
+
filelist.collect do |arg|
|
49
|
+
if File.directory?(arg)
|
50
|
+
Dir["#{arg}/**/*"]
|
51
|
+
else
|
52
|
+
arg
|
53
|
+
end
|
54
|
+
end.flatten.reject { |arg| File.directory?(arg) }
|
55
|
+
end
|
42
56
|
end
|
43
57
|
end
|
data/lib/melon/version.rb
CHANGED
metadata
CHANGED
@@ -1,13 +1,13 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: melon
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
hash:
|
4
|
+
hash: 19
|
5
5
|
prerelease: false
|
6
6
|
segments:
|
7
7
|
- 0
|
8
|
-
-
|
8
|
+
- 3
|
9
9
|
- 0
|
10
|
-
version: 0.
|
10
|
+
version: 0.3.0
|
11
11
|
platform: ruby
|
12
12
|
authors:
|
13
13
|
- Andrew Roberts
|
@@ -15,7 +15,7 @@ autorequire:
|
|
15
15
|
bindir: bin
|
16
16
|
cert_chain: []
|
17
17
|
|
18
|
-
date: 2011-01-
|
18
|
+
date: 2011-01-28 00:00:00 -05:00
|
19
19
|
default_executable: melon
|
20
20
|
dependencies:
|
21
21
|
- !ruby/object:Gem::Dependency
|
@@ -88,6 +88,12 @@ files:
|
|
88
88
|
- lib/melon.rb
|
89
89
|
- lib/melon/cli.rb
|
90
90
|
- lib/melon/commands.rb
|
91
|
+
- lib/melon/commands/add.rb
|
92
|
+
- lib/melon/commands/base.rb
|
93
|
+
- lib/melon/commands/check.rb
|
94
|
+
- lib/melon/commands/help.rb
|
95
|
+
- lib/melon/commands/list.rb
|
96
|
+
- lib/melon/commands/show.rb
|
91
97
|
- lib/melon/example.rb
|
92
98
|
- lib/melon/hasher.rb
|
93
99
|
- lib/melon/helpers.rb
|