melon 0.1.0 → 0.2.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 CHANGED
@@ -1,3 +1,10 @@
1
+ === 0.2.0 2011-01-26
2
+ * Major improvements:
3
+ * added --recursive flag to add subcommand
4
+ * added 'list' subcommand
5
+ * added 'show' subcommand
6
+ * cleaned up help text
7
+
1
8
  === 0.1.0 2011-01-25
2
9
 
3
10
  * Initial release, basic functionality present:
@@ -0,0 +1,57 @@
1
+ Feature: Adding files to the database
2
+ In order to have something to compare new files to
3
+ As a user
4
+ I should be able to add files to the database
5
+
6
+
7
+ Background:
8
+ Given a file named "test_file" with:
9
+ """
10
+ This file is a test file
11
+ """
12
+
13
+ Scenario: Adding files to a melon database
14
+ When I run "melon -d test.db add test_file"
15
+ Then the output should contain a hash
16
+ And the output should contain "test_file"
17
+
18
+ Scenario: Adding a file that already exists
19
+ When I run "melon -d test.db add test_file"
20
+ And I run "melon -d test.db add test_file"
21
+ Then it should fail with:
22
+ """
23
+ melon: path already present in database
24
+ """
25
+
26
+ Scenario: Adding a directory itself
27
+ Given a directory named "testo"
28
+ When I run "melon -d test.db add testo"
29
+ Then it should fail with:
30
+ """
31
+ directory
32
+ """
33
+
34
+ Scenario: Adding a file that doesn't exist
35
+ When I run "melon -d test.db add nonexistant_file"
36
+ Then it should fail with:
37
+ """
38
+ melon: no such file: nonexistant_file
39
+ """
40
+
41
+ Scenario: Adding a directory recursively
42
+ Given a file named "dir/test1" with:
43
+ """
44
+ First test file
45
+ """
46
+ And a file named "dir/test2" with:
47
+ """
48
+ Second test file
49
+ """
50
+ And a file named "dir/test/test3" with:
51
+ """
52
+ Third test file
53
+ """
54
+ When I run "melon -d test.db add -r dir"
55
+ Then the output should contain "dir/test1"
56
+ And the output should contain "dir/test2"
57
+ And the output should contain "dir/test/test3"
@@ -0,0 +1,27 @@
1
+ Feature: Check
2
+ In order to see if a given file is tracked by the database
3
+ As a user
4
+ I should be able to query the database with a file
5
+
6
+ Background:
7
+ Given a file named "test_file" with:
8
+ """
9
+ This file is a test file
10
+ """
11
+
12
+ Scenario: Checking a file that is not in the database
13
+ When I run "melon -d test.db check test_file"
14
+ Then the output should contain "test_file"
15
+ And the output should start with "/"
16
+
17
+ Scenario: Checking a file that is in the database:
18
+ When I run "melon -d test.db add -q test_file"
19
+ And I run "melon -d test.db check test_file"
20
+ Then the output should be empty
21
+
22
+ Scenario: Checking a file that doesn't exist
23
+ When I run "melon -d test.db check nonexistant_file"
24
+ Then it should fail with:
25
+ """
26
+ melon: no such file: nonexistant_file
27
+ """
@@ -7,7 +7,14 @@ Feature: Edge cases
7
7
  """
8
8
  When I run "cp test_file test_file_2"
9
9
  And I run "melon -d test.db add test_file test_file_2"
10
- Then the stderr should contain:
10
+ Then it should fail with:
11
11
  """
12
12
  melon: file exists elsewhere in the database
13
13
  """
14
+
15
+ Scenario: Unrecognized command
16
+ When I run "melon whizzle bang"
17
+ Then it should fail with:
18
+ """
19
+ melon: unrecognized command: whizzle
20
+ """
@@ -0,0 +1,18 @@
1
+ Feature: List
2
+ In order to see what's tracked by the database
3
+ As a user
4
+ I should be able to get a list of tracked files
5
+
6
+ Scenario: Listing files
7
+ Given a file named "test_file" with:
8
+ """
9
+ Test file 1
10
+ """
11
+ And a file named "file_test" with:
12
+ """
13
+ Test file 2
14
+ """
15
+ And I run "melon -d test.db add -q test_file file_test"
16
+ When I run "melon -d test.db list"
17
+ Then the output should contain "test_file"
18
+ And the output should contain "file_test"
@@ -0,0 +1,28 @@
1
+ Feature: Show
2
+ In order to see where a file is stored (according the database)
3
+ As a user
4
+ I should be able to query the database with a file
5
+
6
+ Background:
7
+ Given a file named "dir/test_file" with:
8
+ """
9
+ This file is a test file
10
+ """
11
+ And I run "cp dir/test_file ."
12
+
13
+ Scenario: Showing a file that is not in the database
14
+ When I run "melon -d test.db show test_file"
15
+ Then the output should be empty
16
+
17
+ Scenario: Showing a file that is in the database:
18
+ When I run "melon -d test.db add -q dir/test_file"
19
+ And I run "melon -d test.db show test_file"
20
+ Then the output should contain "dir/test_file"
21
+ And the output should start with "/"
22
+
23
+ Scenario: Showing a file that doesn't exist
24
+ When I run "melon -d test.db show nonexistant_file"
25
+ Then it should fail with:
26
+ """
27
+ melon: no such file: nonexistant_file
28
+ """
data/lib/melon/cli.rb CHANGED
@@ -1,11 +1,14 @@
1
1
  require 'ostruct'
2
+ require 'pstore'
2
3
  require 'optparse'
3
4
 
4
5
  require 'melon/version'
5
6
  require 'melon/commands'
7
+ require 'melon/helpers'
6
8
 
7
9
  module Melon
8
10
  class CLI
11
+ include Helpers
9
12
 
10
13
  def self.execute(arguments=[])
11
14
  new(arguments).run
@@ -42,15 +45,17 @@ module Melon
42
45
  # look for command class in args.shift
43
46
  command_name = arguments.shift
44
47
  begin
45
- command = Commands.const_get(command_name.capitalize)
46
- command.new(arguments, options).run
47
- # rescue NameError
48
- # CLI.error "unrecognized command: #{command_name}"
48
+ c = Commands[command_name.capitalize]
49
+ rescue NameError => e
50
+ # don't swallow NoMethodErrors
51
+ raise e unless e.instance_of?(NameError)
52
+ error "unrecognized command: #{command_name}"
49
53
  end
54
+ c.new(arguments, options).run
50
55
  end
51
56
 
52
57
  def parse_options
53
- options = CLI.default_options
58
+ options = self.class.default_options
54
59
 
55
60
  parser = OptionParser.new do |p|
56
61
  p.banner = "Usage: melon [options] COMMAND [command-options] [ARGS]"
@@ -82,40 +87,15 @@ module Melon
82
87
  puts p
83
88
  exit 0
84
89
  end
85
-
86
90
  end
87
91
 
88
92
  begin
89
93
  parser.order!(arguments)
90
94
  rescue OptionParser::ParseError => e
91
- CLI.error e
95
+ error e
92
96
  end
93
97
 
94
98
  options
95
99
  end
96
-
97
- def format_command(name, desc, margin = 4, width = 22, wrapdesc = 80)
98
- pad = "\n" + ' ' * width
99
- desc = wrap_text(desc, wrapdesc - width).split("\n").join(pad)
100
-
101
- ' ' * margin + "#{name.ljust(width-margin)}#{desc}"
102
- end
103
-
104
- def wrap_text(txt, col = 80)
105
- txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
106
- "\\1\\3\n")
107
- end
108
-
109
-
110
- def self.error(error_obj_or_str, code = 1)
111
- if error_obj_or_str.respond_to?('to_s')
112
- error_str = error_obj_or_str.to_s
113
- else
114
- error_str = error_obj_or_str.inspect
115
- end
116
-
117
- $stderr.puts "melon: #{error_str}"
118
- exit code
119
- end
120
100
  end
121
101
  end
@@ -1,16 +1,32 @@
1
- require 'pstore'
2
- require 'ftools'
3
-
4
1
  require 'melon/hasher'
5
- require 'melon/cli'
6
-
7
- # TODO: in commands, parse arguments with parse! in CLI, parse with order!
2
+ require 'melon/helpers'
8
3
 
9
4
  module Melon
10
5
  module Commands
6
+ def self.each
7
+ consts = []
8
+ base = self.const_get('Base')
9
+ self.constants.each do |c|
10
+ const = self.const_get(c)
11
+ if const.superclass == base
12
+ consts << const
13
+ yield const
14
+ end
15
+ end
16
+ consts
17
+ end
18
+
19
+ class << self
20
+ alias :[] :const_get
21
+ end
22
+
11
23
  # needs a 'verify' command to check integrity of database
24
+ # both internal 2-hash consistency (consistency) and db<->filesystem
25
+ # matching up (integrity) [file exists, hashes match]
12
26
  # needs a 'remove' command, or some way to deal with deletes/renames
27
+ # needs a 'list' command
13
28
  class Base
29
+ include Helpers
14
30
  attr_accessor :args, :options
15
31
  attr_reader :description
16
32
 
@@ -31,10 +47,18 @@ module Melon
31
47
  begin
32
48
  parser.parse!(args)
33
49
  rescue OptionParser::ParseError => e
34
- CLI.error "#{self.class.to_s.split("::").last.downcase}: #{e}"
50
+ error "#{self.class.to_s.split("::").last.downcase}: #{e}"
35
51
  end
52
+
53
+ # verify remaining args are files - overrideable
54
+ verify_args
36
55
  end
37
56
 
57
+ def verify_args
58
+ args.each do |arg|
59
+ error "no such file: #{arg}" unless File.exists?(arg)
60
+ end
61
+ end
38
62
  end
39
63
 
40
64
  class Add < Base
@@ -47,12 +71,20 @@ module Melon
47
71
  @parser ||= OptionParser.new do |p|
48
72
  p.banner = "Usage: melon add [options] file [file [file ...]]"
49
73
  p.separator ""
50
- p.separator Add.description
74
+ p.separator blockquote(Add.description + ".")
51
75
 
52
76
  p.separator ""
53
77
  p.separator "Options:"
54
78
  p.separator ""
55
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
+
56
88
  # p.on("-f", "--force",
57
89
  # "Force the recalculation of the path that",
58
90
  # " already exists in the database") do
@@ -64,29 +96,73 @@ module Melon
64
96
  def run
65
97
  parse_options!
66
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
+
67
109
  options.database.transaction do
68
110
  args.each do |arg|
69
111
  filename = File.expand_path(arg)
70
112
 
71
113
  if File.directory?(filename)
72
- CLI.error "argument is a directory: #{arg}"
114
+ error "argument is a directory: #{arg}"
73
115
  end
74
116
 
75
117
  if options.database[:by_path][filename]# and !options.force
76
- CLI.error "path already present in database: #{arg}"
118
+ error "path already present in database: #{arg}"
77
119
  end
78
120
 
79
121
  # hash strategy should be encapsulated, ergo indirection here
80
122
  hash = Hasher.digest(filename)
81
123
 
82
124
  if options.database[:by_hash][hash]
83
- CLI.error "file exists elsewhere in the database: #{arg}"
125
+ error "file exists elsewhere in the database: #{arg}"
84
126
  end
85
127
 
86
128
 
87
129
  options.database[:by_hash][hash] = filename
88
130
  options.database[:by_path][filename] = hash
89
- puts "#{hash}:#{filename}"
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
90
166
  end
91
167
  end
92
168
  end
@@ -101,7 +177,13 @@ module Melon
101
177
  @parser ||= OptionParser.new do |p|
102
178
  p.banner = "Usage: melon check file [file [file ...]]"
103
179
  p.separator ""
104
- p.separator Check.description
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
+
105
187
  end
106
188
  end
107
189
 
@@ -118,5 +200,35 @@ module Melon
118
200
  end
119
201
  end
120
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
121
233
  end
122
234
  end
@@ -0,0 +1,43 @@
1
+ module Melon
2
+ module Helpers
3
+ def format_command(name, desc, options = {})
4
+ options = {
5
+ :margin => 4,
6
+ :width => 22,
7
+ :wrap => 80
8
+ }.update(options)
9
+
10
+ pad = "\n" + ' ' * options[:width]
11
+ desc = self.wrap_text(desc, options[:wrap] - options[:width])
12
+ desc = desc.split("\n").join(pad)
13
+
14
+ ' ' * options[:margin] +
15
+ "#{name.ljust(options[:width] - options[:margin])}#{desc}"
16
+ end
17
+
18
+ def wrap_text(txt, col = 80)
19
+ txt.gsub(/(.{1,#{col}})( +|$\n?)|(.{1,#{col}})/,
20
+ "\\1\\3\n")
21
+ end
22
+
23
+ def blockquote(string, options = {})
24
+ options = {
25
+ :margin => 4,
26
+ :wrap => 70
27
+ }.update(options)
28
+ options[:width] = options.delete(:margin)
29
+ format_command('', string.gsub(/\s+/,' ').gsub('\. ', '. '), options)
30
+ end
31
+
32
+ def error(error_obj_or_str, code = 1)
33
+ if error_obj_or_str.respond_to?('to_s')
34
+ error_str = error_obj_or_str.to_s
35
+ else
36
+ error_str = error_obj_or_str.inspect
37
+ end
38
+
39
+ $stderr.puts "melon: #{error_str}"
40
+ exit code
41
+ end
42
+ end
43
+ end
data/lib/melon/version.rb CHANGED
@@ -1,6 +1,6 @@
1
1
  module Melon
2
2
  def self.version
3
- "0.1.0"
3
+ "0.2.0"
4
4
  end
5
5
 
6
6
  def self.version_string
data/script/console CHANGED
@@ -1,12 +1,15 @@
1
1
  #!/usr/bin/env ruby
2
- # File: script/console
2
+
3
3
  irb = RUBY_PLATFORM =~ /(:?mswin|mingw)/ ? 'irb.bat' : 'irb'
4
4
 
5
- libs = " -r irb/completion"
6
- libs << " -r irb/ext/save-history"
5
+ lib_dir = File.expand_path(File.join(File.dirname(__FILE__), '..', 'lib'))
6
+
7
+ libs = []
8
+ libs << " -I #{lib_dir}"
9
+ libs << " -r irb/completion"
10
+ libs << " -r irb/ext/save-history"
7
11
 
8
- # Perhaps use a console_lib to store any extra methods I may want available in the cosole
9
- # libs << " -r #{File.dirname(__FILE__) + '/../lib/console_lib/console_logger.rb'}"
10
12
  libs << " -r #{File.dirname(__FILE__) + '/../lib/melon.rb'}"
11
13
  puts "Loading melon gem"
14
+
12
15
  exec "#{irb} #{libs} --simple-prompt"
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: 27
4
+ hash: 23
5
5
  prerelease: false
6
6
  segments:
7
7
  - 0
8
- - 1
8
+ - 2
9
9
  - 0
10
- version: 0.1.0
10
+ version: 0.2.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-25 00:00:00 -05:00
18
+ date: 2011-01-26 00:00:00 -05:00
19
19
  default_executable: melon
20
20
  dependencies:
21
21
  - !ruby/object:Gem::Dependency
@@ -78,18 +78,19 @@ files:
78
78
  - Rakefile
79
79
  - TODO
80
80
  - bin/melon
81
- - features/basic.feature
81
+ - features/add.feature
82
+ - features/check.feature
82
83
  - features/edges.feature
84
+ - features/list.feature
85
+ - features/show.feature
83
86
  - features/step_definitions/basic_steps.rb
84
87
  - features/support/env.rb
85
88
  - lib/melon.rb
86
89
  - lib/melon/cli.rb
87
90
  - lib/melon/commands.rb
88
- - lib/melon/commands/add.rb
89
- - lib/melon/commands/basic_command.rb
90
- - lib/melon/commands/help.rb
91
91
  - lib/melon/example.rb
92
92
  - lib/melon/hasher.rb
93
+ - lib/melon/helpers.rb
93
94
  - lib/melon/version.rb
94
95
  - melon.gemspec
95
96
  - script/console
@@ -133,8 +134,11 @@ signing_key:
133
134
  specification_version: 3
134
135
  summary: A media catalog
135
136
  test_files:
136
- - features/basic.feature
137
+ - features/add.feature
138
+ - features/check.feature
137
139
  - features/edges.feature
140
+ - features/list.feature
141
+ - features/show.feature
138
142
  - features/step_definitions/basic_steps.rb
139
143
  - features/support/env.rb
140
144
  - spec/melon/cli_spec.rb
@@ -1,33 +0,0 @@
1
- Feature: Basic usage
2
-
3
- Background:
4
- Given a file named "test_file" with:
5
- """
6
- This file is a test file
7
- """
8
-
9
- Scenario: Adding files to a melon database
10
- When I run "melon -d test.db add test_file"
11
- Then the output should contain a hash
12
- And the output should contain "test_file"
13
-
14
- Scenario: Adding a file that already exists
15
- When I run "melon -d test.db add test_file"
16
- And I run "melon -d test.db add test_file"
17
- Then the output should contain:
18
- """
19
- melon: path already present in database
20
- """
21
- And the exit status should not be 0
22
-
23
- Scenario: Checking a file that is not in the database
24
- When I run "melon -d test.db check test_file"
25
- Then the output should contain "test_file"
26
- And the output should start with "/"
27
-
28
- Scenario: Adding a directory
29
- Given a directory named "testo"
30
- When I run "melon -d test.db add testo"
31
- Then the output should contain "directory"
32
- And the exit status should not be 0
33
-
@@ -1,10 +0,0 @@
1
- module Melon
2
- module Commands
3
- class Add
4
- extend BasicCommand
5
-
6
-
7
- end
8
- end
9
- end
10
-
@@ -1,19 +0,0 @@
1
- module Melon
2
- module Commands
3
- module BasicCommand
4
-
5
- def execute(arguments, options)
6
- new(arguments, options).run
7
- end
8
-
9
- # returns name {padding} description, for usage"
10
- def short_usage(padding=" ")
11
- name = Melon::Commands.translate_command(self)
12
- extra_spaces = Melon::Commands.commands.collect do |c|
13
- c.length
14
- end.max - name.length
15
- "#{name}#{padding}#{' ' * extra_spaces}#{self.description}"
16
- end
17
- end
18
- end
19
- end
@@ -1,51 +0,0 @@
1
- require 'melon/commands/basic_command'
2
-
3
- module Melon
4
- module Commands
5
- class Help
6
- # Help is a basic command that also ties itself in to cli#usage, so
7
- # it's a little bit unconventional. Retrospectively, I should not
8
- # have written it first.
9
- extend BasicCommand
10
-
11
- def self.description
12
- "Get help with a specific command, or with Melon in general"
13
- end
14
-
15
- attr_accessor :arguments
16
-
17
- def initialize(arguments, options)
18
- self.arguments = arguments
19
- end
20
-
21
- def parser
22
- @parser ||= OptionParser.new do |opts|
23
- Melon::Commands.command_hash.each do |name, command|
24
- next if command == self.class
25
- # TODO help banner: gem help help
26
- # TODO flesh out parsing - give short_usage for each command
27
- opts.on(name) { command.parser }
28
- end
29
- end
30
- end
31
-
32
- def run
33
- begin
34
- parser.parse!(arguments)
35
- rescue OptionParser::InvalidOption => e
36
- puts "melon: #{e.to_s}"
37
- exit 1
38
- end
39
-
40
- if arguments == ['help']
41
- puts parser
42
- exit
43
- end
44
-
45
- # if arguments are empty, we handled it in CLI
46
- puts "melon: '#{arguments.join(' ')}' is not a recognized command."
47
- exit 1
48
- end
49
- end
50
- end
51
- end