melon 0.2.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
data/History.txt CHANGED
@@ -1,3 +1,7 @@
1
+ === 0.3.0 2011-01-28
2
+ * Major improvements:
3
+ * added 'help' command'
4
+
1
5
  === 0.2.0 2011-01-26
2
6
  * Major improvements:
3
7
  * added --recursive flag to add subcommand
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
- When I run "melon -d test.db add -r dir"
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
- %w(add check).each do |command|
68
- cls = Commands.const_get(command.capitalize)
69
- p.separator format_command(command, cls.description)
70
- end
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
@@ -1,14 +1,13 @@
1
- require 'melon/hasher'
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
- base = self.const_get('Base')
9
- self.constants.each do |c|
7
+ self.constants.sort.each do |c|
10
8
  const = self.const_get(c)
11
- if const.superclass == base
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
- # needs a 'verify' command to check integrity of database
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
- # needs a 'list' command
28
- class Base
29
- include Helpers
30
- attr_accessor :args, :options
31
- attr_reader :description
32
-
33
- def initialize(args, options)
34
- self.args = args
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
@@ -3,6 +3,7 @@ require 'digestif/hasher'
3
3
 
4
4
  module Melon
5
5
  class Hasher
6
+ # TODO: scale seek size up with file size
6
7
  def self.digest(filename)
7
8
  hasher = Digestif::Hasher.new(filename)
8
9
  hasher.options.read_size = 40000
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
- format_command('', string.gsub(/\s+/,' ').gsub('\. ', '. '), options)
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
@@ -1,6 +1,6 @@
1
1
  module Melon
2
2
  def self.version
3
- "0.2.0"
3
+ "0.3.0"
4
4
  end
5
5
 
6
6
  def self.version_string
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: 23
4
+ hash: 19
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 2
8
+ - 3
9
9
  - 0
10
- version: 0.2.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-26 00:00:00 -05:00
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