treet 0.8.2 → 0.10.2

Sign up to get free protection for your applications and to get access to all the features.
data/.gitignore CHANGED
@@ -15,4 +15,3 @@ spec/reports
15
15
  test/tmp
16
16
  test/version_tmp
17
17
  tmp
18
- *.json
data/.travis.yml ADDED
@@ -0,0 +1,4 @@
1
+ language: ruby
2
+ rvm:
3
+ - 1.9.3
4
+ - rbx-19mode
data/Gemfile CHANGED
@@ -2,5 +2,3 @@ source 'https://rubygems.org'
2
2
 
3
3
  # Specify your gem's dependencies in treet.gemspec
4
4
  gemspec
5
-
6
- gem 'thor'
data/Guardfile CHANGED
@@ -1,9 +1,28 @@
1
1
  # A sample Guardfile
2
2
  # More info at https://github.com/guard/guard#readme
3
3
 
4
+ guard 'minitest', test_folders: 'spec', test_file_patterns: 'test_*.rb' do
5
+ # with Minitest::Unit
6
+ # watch(%r{^spec/lib/test_.*\.rb})
7
+
8
+ # watch(%r|^test/(.*)\/?test_(.*)\.rb|)
9
+ # watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "test/#{m[1]}test_#{m[2]}.rb" }
10
+
11
+ # watch(%r|^lib/otherbase/contacts?.rb|) { "test" }
12
+
13
+ # with Minitest::Spec
14
+ watch(%r|^spec/lib/test_(.*)\.rb|)
15
+ watch(%r{^lib/treet/(.+)\.rb$}) { |m| "spec/lib/test_#{m[1]}.rb" }
16
+ watch(%r|^spec/test_helper\.rb|) { "spec" }
17
+ # watch(%r|^lib/(.*)([^/]+)\.rb|) { |m| "spec/#{m[1]}#{m[2]}_spec.rb" }
18
+ # watch(%r|^spec/spec_helper\.rb|) { "spec" }
19
+ end
20
+
4
21
  guard 'rspec', :version => 2 do
5
22
  watch(%r{^spec/.+_spec\.rb$})
23
+
6
24
  watch(%r{^lib/treet/(.+)\.rb$}) { |m| "spec/lib/#{m[1]}_spec.rb" }
7
25
  watch(%r{^(.+)\.rb$}) { |m| "spec/#{m[1]}_spec.rb" }
8
26
  watch('spec/spec_helper.rb') { "spec" }
9
27
  end
28
+
data/lib/treet/farm.rb CHANGED
@@ -1,23 +1,24 @@
1
1
  # encoding: UTF-8
2
2
 
3
- require "uuidtools"
3
+ require "securerandom"
4
4
 
5
5
  class Treet::Farm
6
- attr_reader :root, :xrefkey
6
+ attr_reader :root, :xrefkey, :repotype
7
7
 
8
8
  def initialize(opts)
9
9
  raise Errno::ENOENT unless File.directory?(opts[:root])
10
10
 
11
11
  @root = opts[:root]
12
12
  @xrefkey = opts[:xref]
13
+ @repotype = opts[:repotype] || Treet::Repo
13
14
  end
14
15
 
15
- def repos
16
+ def repos(opts = {})
16
17
  @repos_cache ||= Dir.glob("#{root}/*").each_with_object({}) do |subdir,h|
17
18
  # in a Farm we are looking for repositories under the root
18
19
  if File.directory?(subdir)
19
20
  xref = File.basename(subdir)
20
- h[xref] = Treet::Repo.new(subdir, :xrefkey => xrefkey, :xref => xref)
21
+ h[xref] = repotype.new(subdir, opts.merge(:xrefkey => xrefkey, :xref => xref))
21
22
  end
22
23
  end
23
24
  end
@@ -41,13 +42,13 @@ class Treet::Farm
41
42
  array_of_hashes = JSON.load(File.open(jsonfile))
42
43
  Dir.chdir(rootdir) do
43
44
  array_of_hashes.each do |h|
44
- uuid = UUIDTools::UUID.random_create.to_s
45
+ uuid = SecureRandom.uuid
45
46
  thash = Treet::Hash.new(h)
46
- thash.to_repo(uuid)
47
+ thash.to_repo(uuid, opts)
47
48
  end
48
49
  end
49
50
 
50
- Treet::Farm.new(:root => rootdir, :xref => opts[:xref])
51
+ self.new(opts)
51
52
  end
52
53
 
53
54
  # apply patches to a farm of repos
@@ -62,9 +63,11 @@ class Treet::Farm
62
63
  end
63
64
 
64
65
  # add a new repo, with data from an input hash
65
- def add(hash)
66
- uuid = UUIDTools::UUID.random_create.to_s
66
+ # if an :id is provided, then the new repo will be stored under that directory name,
67
+ # otherwise a unique id will be generated
68
+ def add(hash, opts = {})
69
+ uuid = opts[:id] || UUIDTools::UUID.random_create.to_s
67
70
  thash = Treet::Hash.new(hash)
68
- thash.to_repo("#{root}/#{uuid}")
71
+ repos[uuid] = thash.to_repo("#{root}/#{uuid}", opts.merge(:repotype => repotype))
69
72
  end
70
73
  end
@@ -0,0 +1,28 @@
1
+ # encoding: UTF-8
2
+
3
+ class Treet::Gitfarm < Treet::Farm
4
+ attr_reader :author
5
+
6
+ def initialize(opts)
7
+ raise ArgumentError, "No git farm without an author for commits" unless opts[:author]
8
+ super
9
+ @repotype = Treet::Gitrepo
10
+ @author = opts[:author]
11
+ end
12
+
13
+ def self.plant(opts)
14
+ super(opts.merge(:repotype => Treet::Gitrepo))
15
+ end
16
+
17
+ def repos
18
+ super(:author => author)
19
+ end
20
+
21
+ def add(hash, opts = {})
22
+ repo = super(hash, opts.merge(:author => author))
23
+ if opts[:tag]
24
+ repo.tag(opts[:tag])
25
+ end
26
+ repo
27
+ end
28
+ end
@@ -0,0 +1,180 @@
1
+ # encoding: UTF-8
2
+
3
+ require "rugged"
4
+ require "forwardable"
5
+
6
+ class Treet::Gitrepo < Treet::Repo
7
+ extend Forwardable
8
+ def_delegators :@gitrepo, :head, :refs, :index
9
+
10
+ def initialize(path, opts = {})
11
+ raise ArgumentError, "author required for updates" unless opts[:author]
12
+ super
13
+
14
+ @author = opts[:author]
15
+
16
+ begin
17
+ @gitrepo = Rugged::Repository.new(root)
18
+ rescue Rugged::RepositoryError
19
+ @gitrepo = initialize_gitrepo
20
+ add_and_commit!
21
+ end
22
+ end
23
+
24
+ def tags
25
+ gitrepo.refs(/tags/)
26
+ end
27
+
28
+ def tag(tagname)
29
+ refname = "refs/tags/#{tagname}"
30
+ begin
31
+ if tag_ref = Rugged::Reference.lookup(gitrepo, refname)
32
+ # move an existing tag
33
+ tag_ref.target = head.target
34
+ else
35
+ # new tag
36
+ Rugged::Reference.create(gitrepo, refname, head.target)
37
+ end
38
+ rescue Rugged::ReferenceError
39
+ # invalid string for source, e.g. blank or illegal punctuation (colons)
40
+ raise ArgumentError "invalid source string '#{tagname}' for repository tagging"
41
+ end
42
+ end
43
+
44
+ def patch(patchdef)
45
+ super
46
+ add_and_commit!
47
+ end
48
+
49
+ def to_hash(opts = {})
50
+ if opts[:commit]
51
+ snapshot(opts[:commit])
52
+ elsif opts[:tag]
53
+ tag_snapshot(opts[:tag])
54
+ else
55
+ # snapshot(head.target)
56
+ super()
57
+ end
58
+ end
59
+
60
+ def entries
61
+ index.entries.map{|e| e[:path]}
62
+ end
63
+
64
+ def branches
65
+ refs(/heads/).map(&:name) - ['refs/heads/master']
66
+ end
67
+
68
+ # always branch from tip of master (private representation)
69
+ def branch(name)
70
+ Rugged::Reference.create(gitrepo, "refs/heads/#{name}", head.target)
71
+ end
72
+
73
+ def version(opts = {})
74
+ if tag = opts[:tag]
75
+ ref = Rugged::Reference.lookup(gitrepo, "refs/tags/#{tag}")
76
+ ref && ref.target
77
+ else
78
+ head.target
79
+ end
80
+ end
81
+
82
+ private
83
+
84
+ attr_reader :gitrepo, :author
85
+
86
+ def initialize_gitrepo
87
+ Rugged::Repository.init_at(root, false)
88
+ end
89
+
90
+ # always commits to HEAD
91
+ def commit!(sha)
92
+ parent_shas = begin
93
+ [gitrepo.head.target]
94
+ rescue Rugged::ReferenceError
95
+ # this is the first commit
96
+ []
97
+ end
98
+
99
+ authorship = author.merge(:time => Time.now)
100
+
101
+ sha = Rugged::Commit.create(gitrepo,
102
+ :message => "",
103
+ :author => authorship,
104
+ :committer => authorship,
105
+ :parents => parent_shas,
106
+ :tree => sha,
107
+ :update_ref => "HEAD"
108
+ )
109
+
110
+ sha
111
+ end
112
+
113
+ def add_and_commit!
114
+ current_index = entries
115
+ Dir.chdir(root) do
116
+ current_files = Dir.glob('**/*')
117
+
118
+ # additions
119
+ (current_files - current_index).each do |file|
120
+ # must add each filename explicitly, `index#add` does not recurse into directories
121
+ if File.file?(file)
122
+ index.add(file)
123
+ end
124
+ end
125
+
126
+ # possible alterations - these changes won't be detected unless we explicitly git-add
127
+ (current_files & current_index).each do |file|
128
+ if File.file?(file)
129
+ index.add(file)
130
+ end
131
+ end
132
+
133
+ # deletions
134
+ (current_index - current_files).each do |file|
135
+ # `index#remove` handles directories
136
+ index.remove(file)
137
+ end
138
+ end
139
+
140
+ index.write
141
+ tree_sha = index.write_tree
142
+ commit!(tree_sha)
143
+ end
144
+
145
+ def snapshot(commit_sha)
146
+ commit = gitrepo.lookup(commit_sha)
147
+ tree = commit.tree
148
+ # must traverse the tree: entries are files or subtrees
149
+ data = {}
150
+ tree.each do |obj|
151
+ data[obj[:name]] = case obj[:type]
152
+ when :blob
153
+ begin
154
+ JSON.load(gitrepo.read(obj[:oid]).data)
155
+ rescue JSON::ParserError
156
+ raise JSON::ParserError, "bad JSON in blob #{obj[:name]}: #{gitrepo.read(obj[:oid]).data}"
157
+ end
158
+ when :tree
159
+ begin
160
+ gitrepo.lookup(obj[:oid]).each_with_object([]) do |subobj,d|
161
+ d << JSON.load(gitrepo.read(subobj[:oid]).data)
162
+ end
163
+ rescue JSON::ParserError
164
+ raise JSON::ParserError, "bad JSON in tree #{obj[:name]}: #{gitrepo.read(obj[:oid]).data}"
165
+ end
166
+ else
167
+ raise TypeError, "UNRECOGNIZED GIT OBJECT TYPE #{obj[:type]}"
168
+ end
169
+ end
170
+
171
+ # decorate(data)
172
+ data
173
+ end
174
+
175
+ def tag_snapshot(tagname)
176
+ tag_ref = Rugged::Reference.lookup(gitrepo, "refs/tags/#{tagname}")
177
+ raise ArgumentError, "tag '#{tagname}' does not exist in this repo" unless tag_ref
178
+ snapshot(tag_ref.target)
179
+ end
180
+ end
data/lib/treet/hash.rb CHANGED
@@ -22,9 +22,10 @@ class Treet::Hash
22
22
  @data = normalize(d)
23
23
  end
24
24
 
25
- def to_repo(root)
25
+ def to_repo(root, opts = {})
26
26
  construct(data, root)
27
- Treet::Repo.new(root)
27
+ repotype = opts[:repotype] || Treet::Repo
28
+ repotype.new(root, opts)
28
29
  end
29
30
 
30
31
  def to_hash
@@ -42,8 +43,13 @@ class Treet::Hash
42
43
  Treet::Hash.new(newhash)
43
44
  end
44
45
 
45
- def self.digestify(hash)
46
- Digest::SHA1.hexdigest(hash.to_a.sort.flatten.join)
46
+ def self.digestify(data)
47
+ case data
48
+ when Hash
49
+ Digest::SHA1.hexdigest(data.to_a.sort.flatten.join)
50
+ else # String
51
+ data
52
+ end
47
53
  end
48
54
 
49
55
  private
@@ -66,7 +72,7 @@ class Treet::Hash
66
72
  case v2
67
73
  when String
68
74
  # create empty file with this name
69
- File.open("#{k}/#{v2}", "w")
75
+ FileUtils.touch("#{k}/#{v2}")
70
76
 
71
77
  else
72
78
  # store object contents as JSON into a generated filename
data/lib/treet/repo.rb CHANGED
@@ -1,25 +1,24 @@
1
1
  # encoding: UTF-8
2
2
 
3
- # require 'hashdiff'
4
-
5
3
  class Treet::Repo
6
- attr_reader :root, :hash, :opts
4
+ attr_reader :root
7
5
 
8
6
  def initialize(path, opts = {})
9
- # TODO: validate that path exists and is a directory (symlinks should work)
7
+ raise "Missing or invalid source path #{path}" unless File.directory?(path)
10
8
 
11
9
  @root = path
12
- raise "Missing or invalid source path #{path}" unless File.directory?(path)
13
- @opts = opts
14
10
  end
15
11
 
16
12
  def to_hash
17
- @hash ||= expand(root)
13
+ expand(root)
14
+ end
15
+
16
+ def reset
17
+ # clear any cached data
18
18
  end
19
19
 
20
20
  def compare(target)
21
21
  Treet::Hash.diff(to_hash, target.to_hash)
22
- # HashDiff.diff(to_hash, hash)
23
22
  end
24
23
 
25
24
  # patch keys can look like
@@ -28,14 +27,14 @@ class Treet::Repo
28
27
  # (address[1] syntax has been eliminated, we recognize array elements by matching the entire content)
29
28
  def self.filefor(keyname)
30
29
  if keyname =~ /\[/
31
- keyname, is_array, index = keyname.match(/^(.*)(\[\])$/).captures
30
+ keyname = keyname.match(/^(.*)\[\]$/).captures.first
32
31
  [keyname, '', nil]
33
32
  elsif keyname =~ /\./
34
33
  # subelement
35
34
  filename,field = keyname.split('.')
36
35
  ['.', filename, field]
37
36
  else
38
- [nil, keyname]
37
+ ['.', keyname]
39
38
  end
40
39
  end
41
40
 
@@ -47,24 +46,31 @@ class Treet::Repo
47
46
  Dir.chdir(root) do
48
47
  diffs.each do |diff|
49
48
  flag, key, v1, v2 = diff
50
- if key =~ /\[/
51
- keyname, is_array = key.match(/^(.*)(\[\])$/).captures
52
- elsif key =~ /\./
53
- keyname, subkey = key.match(/^(.*)\.(.*)$/).captures
54
- else
55
- keyname = key
56
- end
49
+ # if key =~ /\[/
50
+ # keyname = key.match(/^(.*)\[\]$/).captures
51
+ # elsif key =~ /\./
52
+ # keyname, subkey = key.match(/^(.*)\.(.*)$/).captures
53
+ # else
54
+ # keyname = key
55
+ # end
57
56
 
58
57
  dirname, filename, fieldname = Treet::Repo.filefor(key)
59
58
  filepath = "#{dirname}/#{filename}"
59
+
60
60
  case flag
61
61
  when '~'
62
62
  # change a value in place
63
63
  # load the current data & overwrite with the new value
64
64
  # idempotent: this will overwrite the file with the same contents
65
- data = File.exists?(filepath) ? JSON.load(File.open(filepath)) : {}
66
- data[fieldname] = v1
67
- File.open(filepath, "w") {|f| f << JSON.pretty_generate(data)}
65
+ if fieldname
66
+ # hash entry
67
+ data = File.exists?(filepath) ? JSON.load(File.open(filepath)) : {}
68
+ data[fieldname] = v1
69
+ File.open(filepath, "w") {|f| f << JSON.pretty_generate(data)}
70
+ else
71
+ # string entry
72
+ File.open(filepath, "w") {|f| f << v1}
73
+ end
68
74
 
69
75
  when '+'
70
76
  # add something
@@ -80,19 +86,31 @@ class Treet::Repo
80
86
  # idempotent: this will overwrite the file with the same contents
81
87
  subfile = "#{dirname}/#{Treet::Hash.digestify(v1)}"
82
88
  Dir.mkdir(dirname) unless Dir.exists?(dirname)
83
- File.open(subfile, "w") {|f| f << JSON.pretty_generate(v1)}
89
+ case v1
90
+ when Hash
91
+ # hash entry
92
+ File.open(subfile, "w") {|f| f << JSON.pretty_generate(v1)}
93
+ else
94
+ # string entry - create empty file with this name
95
+ FileUtils.touch(subfile)
96
+ end
84
97
  end
85
98
 
86
99
  when '-'
87
100
  # remove something
88
101
  if fieldname
102
+ # this is a key in a subhash
89
103
  data = JSON.load(File.open(filepath))
90
104
  data.delete(fieldname)
91
105
  if data.empty?
106
+ # all keys have been removed, clean up the file
92
107
  File.delete(filename)
93
108
  else
94
109
  File.open(filepath, "w") {|f| f << JSON.pretty_generate(data)}
95
110
  end
111
+ elsif dirname == "."
112
+ # this is a top-level string
113
+ File.delete(filename) if File.exists?(filename) # need the existence check for idempotence
96
114
  else
97
115
  # this is an array, we look for a match on the entire contents via digest
98
116
  subfile = "#{dirname}/#{Treet::Hash.digestify(v1)}"
@@ -130,12 +148,6 @@ class Treet::Repo
130
148
 
131
149
  def expand(path)
132
150
  files = Dir.entries(path).select {|f| f !~ /^\./}
133
- hash = files.each_with_object({}) {|f,h| h[f] = expand_json("#{path}/#{f}")}
134
-
135
- if opts[:xrefkey]
136
- hash['xref'] ||= {}
137
- hash['xref'][opts[:xrefkey]] = opts[:xref]
138
- end
139
- hash
151
+ files.each_with_object({}) {|f,h| h[f] = expand_json("#{path}/#{f}")}
140
152
  end
141
153
  end
data/lib/treet/version.rb CHANGED
@@ -1,3 +1,3 @@
1
1
  module Treet
2
- VERSION = "0.8.2"
2
+ VERSION = "0.10.2"
3
3
  end
data/lib/treet.rb CHANGED
@@ -6,8 +6,13 @@ unless Kernel.respond_to?(:require_relative)
6
6
  end
7
7
  end
8
8
 
9
- require_relative "treet/version"
9
+ %w(version hash repo farm).each do |f|
10
+ require_relative "treet/#{f}"
11
+ end
10
12
 
11
- require_relative "treet/repo"
12
- require_relative "treet/hash"
13
- require_relative "treet/farm"
13
+ # under MacRuby 0.12, the `rugged` gem causes seg fault, sometimes merely on require()
14
+ unless defined? MACRUBY_VERSION
15
+ %w(gitrepo gitfarm).each do |f|
16
+ require_relative "treet/#{f}"
17
+ end
18
+ end
@@ -0,0 +1,8 @@
1
+ {
2
+ "title": "This is the title",
3
+ "datalist": [
4
+ "one",
5
+ "two",
6
+ "three"
7
+ ]
8
+ }
@@ -9,6 +9,17 @@ describe "Repo" do
9
9
  }
10
10
  end
11
11
 
12
+ it "should reload on reset" do
13
+ repo = Treet::Repo.new("#{File.dirname(__FILE__)}/../repos/one")
14
+ repo.to_hash.should == {
15
+ 'name' => {'full' => 'John Bigbooté'}
16
+ }
17
+ repo.reset
18
+ repo.to_hash.should == {
19
+ 'name' => {'full' => 'John Bigbooté'}
20
+ }
21
+ end
22
+
12
23
  it "should generate optional " do
13
24
  repo = Treet::Repo.new("#{File.dirname(__FILE__)}/../repos/one")
14
25
  repo.to_hash.should == {
@@ -41,15 +52,16 @@ describe "Repo" do
41
52
  it "should generate file paths correctly from key paths in patches" do
42
53
  Treet::Repo.filefor("name.first").should == [".", "name", "first"]
43
54
  Treet::Repo.filefor("emails[]").should == ['emails', "", nil]
55
+ Treet::Repo.filefor("topvalue").should == ['.', "topvalue"]
44
56
  end
45
57
 
46
- it "should added xref keys when specified" do
47
- repo = Treet::Repo.new("#{File.dirname(__FILE__)}/../repos/one", :xrefkey => 'foo', :xref => 'bar')
48
- repo.to_hash.should == {
49
- 'name' => {'full' => 'John Bigbooté'},
50
- 'xref' => {'foo' => 'bar'}
51
- }
52
- end
58
+ # it "should add xref keys when specified" do
59
+ # repo = Treet::Repo.new("#{File.dirname(__FILE__)}/../repos/one", :xrefkey => 'foo', :xref => 'bar')
60
+ # repo.to_hash.should == {
61
+ # 'name' => {'full' => 'John Bigbooté'},
62
+ # 'xref' => {'foo' => 'bar'}
63
+ # }
64
+ # end
53
65
 
54
66
  it "should take patches that add values to missing elements" do
55
67
  hash = Treet::Hash.new("#{File.dirname(__FILE__)}/../json/one.json")
@@ -88,4 +100,100 @@ describe "Repo" do
88
100
  newhash['foo']['bar'].should == 'new value'
89
101
  end
90
102
  end
103
+
104
+ it "should accept patches that update top-level values" do
105
+ hash = Treet::Hash.new("#{File.dirname(__FILE__)}/../json/four.json")
106
+ Dir.mktmpdir do |dir|
107
+ repo = hash.to_repo(dir)
108
+ repo.patch([
109
+ [
110
+ "~",
111
+ "title",
112
+ "Updated Title"
113
+ ]
114
+ ])
115
+ newhash = repo.to_hash
116
+ newhash['title'].should == 'Updated Title'
117
+
118
+ repo.patch([
119
+ [
120
+ "-",
121
+ "datalist[]",
122
+ "two"
123
+ ]
124
+ ])
125
+ newhash = repo.to_hash
126
+ newhash['datalist'].count.should == 2
127
+ end
128
+ end
129
+
130
+ it "should add hash entries inside existing arrays" do
131
+ hash = Treet::Hash.new(load_json('two'))
132
+ Dir.mktmpdir do |dir|
133
+ repo = hash.to_repo(dir)
134
+ repo.patch([
135
+ [
136
+ "+",
137
+ "emails[]",
138
+ {"label" => "home", "label" => "myname@gmail.com"}
139
+ ]
140
+ ])
141
+ newhash = repo.to_hash
142
+ newhash['emails'].count.should == 3
143
+ newhash['emails'].each {|x| x.is_a?(Hash).should be_true}
144
+ end
145
+ end
146
+
147
+ it "should clean up after a subhash deletion patch" do
148
+ hash = Treet::Hash.new("#{File.dirname(__FILE__)}/../json/one.json")
149
+ Dir.mktmpdir do |dir|
150
+ repo = hash.to_repo(dir)
151
+ repo.patch([
152
+ [
153
+ "-",
154
+ "name.full",
155
+ "oldval"
156
+ ]
157
+ ])
158
+ newhash = repo.to_hash
159
+ newhash.keys.should be_empty
160
+ end
161
+ end
162
+
163
+ it "should clean up after a top-level string deletion patch" do
164
+ hash = Treet::Hash.new("#{File.dirname(__FILE__)}/../json/four.json")
165
+ Dir.mktmpdir do |dir|
166
+ repo = hash.to_repo(dir)
167
+ repo.patch([
168
+ [
169
+ "-",
170
+ "title",
171
+ "oldval"
172
+ ]
173
+ ])
174
+ newhash = repo.to_hash
175
+ newhash.keys.should == ['datalist']
176
+ end
177
+ end
178
+
179
+ it "should create empty files correctly when patch adds elements" do
180
+ hash = Treet::Hash.new("#{File.dirname(__FILE__)}/../json/four.json")
181
+ Dir.mktmpdir do |dir|
182
+ repo = hash.to_repo(dir)
183
+ repo.patch([
184
+ [
185
+ "+",
186
+ "datalist[]",
187
+ "two"
188
+ ],
189
+ [
190
+ "+",
191
+ "datalist[]",
192
+ "seven"
193
+ ]
194
+ ])
195
+ newhash = repo.to_hash
196
+ newhash.to_hash['datalist'].should be_include('seven')
197
+ end
198
+ end
91
199
  end