melon 0.1.0 → 0.2.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,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