git_dump 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,31 @@
1
+ require 'git_dump/path_object'
2
+ require 'git_dump/tree/base'
3
+ require 'git_dump/entry'
4
+
5
+ class GitDump
6
+ # Interface to git tree
7
+ class Tree < PathObject
8
+ include Base
9
+
10
+ attr_reader :sha
11
+ def initialize(repo, dir, name, sha)
12
+ super(repo, dir, name)
13
+ @sha = sha
14
+ @entries = read_entries
15
+ end
16
+
17
+ private
18
+
19
+ def read_entries
20
+ entries = {}
21
+ repo.tree_entries(sha).each do |entry|
22
+ entries[entry[:name]] = if entry[:type] == :tree
23
+ self.class.new(repo, path, entry[:name], entry[:sha])
24
+ else
25
+ Entry.new(repo, path, entry[:name], entry[:sha], entry[:mode])
26
+ end
27
+ end
28
+ entries
29
+ end
30
+ end
31
+ end
@@ -0,0 +1,54 @@
1
+ require 'git_dump/path_object'
2
+ require 'git_dump/entry'
3
+
4
+ class GitDump
5
+ class Tree < PathObject
6
+ # Common methods in Tree and Builder
7
+ module Base
8
+ # Retrive tree or entry at path, return nil if there is nothing at path
9
+ def [](path)
10
+ get_at(parse_path(path))
11
+ end
12
+
13
+ # Iterate over every tree/entry of this tree, return enumerator if no
14
+ # block given
15
+ def each(&block)
16
+ return to_enum(:each) unless block
17
+ @entries.each do |_, entry|
18
+ block[entry]
19
+ end
20
+ end
21
+
22
+ # Iterate over all entries recursively, return enumerator if no block
23
+ # given
24
+ def each_recursive(&block)
25
+ return to_enum(:each_recursive) unless block
26
+ @entries.each do |_, entry|
27
+ if entry.is_a?(Entry)
28
+ block[entry]
29
+ else
30
+ entry.each_recursive(&block)
31
+ end
32
+ end
33
+ end
34
+
35
+ protected
36
+
37
+ def get_at(parts)
38
+ return unless (entry = @entries[parts.first])
39
+ if parts.length == 1
40
+ entry
41
+ elsif entry.is_a?(self.class)
42
+ entry.get_at(parts.drop(1))
43
+ end
44
+ end
45
+
46
+ private
47
+
48
+ def parse_path(path)
49
+ path = Array(path).join('/') unless path.is_a?(String)
50
+ path.scan(/[^\/]+/)
51
+ end
52
+ end
53
+ end
54
+ end
@@ -0,0 +1,62 @@
1
+ require 'git_dump/path_object'
2
+ require 'git_dump/entry'
3
+
4
+ class GitDump
5
+ class Tree < PathObject
6
+ # Creating tree
7
+ class Builder < PathObject
8
+ include Base
9
+
10
+ def initialize(repo, dir, name)
11
+ super(repo, dir, name)
12
+ @entries = {}
13
+ end
14
+
15
+ # Store data `content` with mode `mode` at `path`
16
+ # Pass `nil` as content to remove
17
+ def store(path, content, mode = 0644)
18
+ put_at(parse_path(path), content && repo.data_sha(content), mode)
19
+ end
20
+ alias_method :[]=, :store
21
+
22
+ # Store data from `from` with mode `mode` (by default file mode) at `path`
23
+ def store_from(path, from, mode = nil)
24
+ mode ||= File.stat(from).mode
25
+ put_at(parse_path(path), repo.path_sha(from), mode)
26
+ end
27
+
28
+ def sha
29
+ repo.treeify(@entries.map do |name, entry|
30
+ attributes = {:name => name, :sha => entry.sha}
31
+ if entry.is_a?(self.class)
32
+ attributes.merge(:type => :tree)
33
+ else
34
+ attributes.merge(:type => :blob, :mode => entry.mode)
35
+ end
36
+ end)
37
+ end
38
+
39
+ def inspect
40
+ "#<#{self.class} #{@entries.inspect}>"
41
+ end
42
+
43
+ protected
44
+
45
+ def put_at(parts, sha, mode)
46
+ name = parts.shift
47
+ if parts.empty?
48
+ if sha.nil?
49
+ @entries.delete(name)
50
+ else
51
+ @entries[name] = Entry.new(repo, path, name, sha, mode)
52
+ end
53
+ else
54
+ unless @entries[name].is_a?(self.class)
55
+ @entries[name] = self.class.new(repo, path, name)
56
+ end
57
+ @entries[name].put_at(parts, sha, mode)
58
+ end
59
+ end
60
+ end
61
+ end
62
+ end
@@ -0,0 +1,55 @@
1
+ require 'git_dump/version/base'
2
+ require 'git_dump/tree'
3
+
4
+ class GitDump
5
+ # Reading version
6
+ class Version
7
+ include Base
8
+
9
+ def self.list(repo)
10
+ repo.tag_entries.map do |entry|
11
+ Version.new(repo, entry[:name], entry[:sha], {
12
+ :time => entry[:author_time],
13
+ :commit_time => entry[:commit_time],
14
+ :annotation => entry[:tag_message],
15
+ :description => entry[:commit_message],
16
+ })
17
+ end
18
+ end
19
+
20
+ def self.by_id(repo, id)
21
+ list(repo).find{ |version| version.id == id }
22
+ end
23
+
24
+ attr_reader :repo, :id, :sha, :time
25
+ attr_reader :commit_time, :annotation, :description
26
+ def initialize(repo, id, sha, attributes = {})
27
+ @repo, @id, @sha = repo, id, sha
28
+ @time = attributes[:time]
29
+ @commit_time = attributes[:commit_time]
30
+ @annotation = attributes[:annotation]
31
+ @description = attributes[:description]
32
+ end
33
+
34
+ # Send this version to repo at url
35
+ # Use :progress => true to show progress
36
+ def push(url, options = {})
37
+ repo.push(url, id, options)
38
+ end
39
+
40
+ # Remove this version
41
+ def remove
42
+ repo.remove_tag(id)
43
+ end
44
+
45
+ def inspect
46
+ "#<#{self.class} id=#{@id} sha=#{@sha} tree=#{@tree.inspect}>"
47
+ end
48
+
49
+ private
50
+
51
+ def tree
52
+ @tree ||= Tree.new(repo, nil, nil, sha)
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,21 @@
1
+ class GitDump
2
+ class Version
3
+ # Common methods in Version and Builder
4
+ module Base
5
+ # Retrive tree or entry at path, return nil if there is nothing at path
6
+ def [](path)
7
+ tree[path]
8
+ end
9
+
10
+ # Iterate over every tree/entry at root level
11
+ def each(&block)
12
+ tree.each(&block)
13
+ end
14
+
15
+ # Iterate over all entries recursively
16
+ def each_recursive(&block)
17
+ tree.each_recursive(&block)
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,77 @@
1
+ require 'git_dump/version'
2
+ require 'git_dump/version/base'
3
+ require 'git_dump/tree/builder'
4
+
5
+ class GitDump
6
+ class Version
7
+ # Creating version
8
+ class Builder
9
+ include Base
10
+
11
+ attr_reader :repo
12
+ def initialize(repo)
13
+ @repo = repo
14
+ @tree = Tree::Builder.new(repo, nil, nil)
15
+ end
16
+
17
+ # Store data `content` with mode `mode` at `path`
18
+ # Pass `nil` as content to remove
19
+ def store(path, content, mode = 0644)
20
+ tree.store(path, content, mode)
21
+ end
22
+ alias_method :[]=, :store
23
+
24
+ # Store data from `from` with mode `mode` (by default file mode) at `path`
25
+ def store_from(path, from, mode = nil)
26
+ tree.store_from(path, from, mode)
27
+ end
28
+
29
+ # Create commit and tag it, returns Version instance
30
+ # Options:
31
+ # :time - set version time (tag and commit)
32
+ # :tags - list of strings to associate with this version
33
+ # :annotation - tag message
34
+ # :description - commit message
35
+ # :keep_identity - don't override identity with "git_dump
36
+ # gitdump@hostname"
37
+ def commit(options = {})
38
+ options = {:time => Time.now}.merge(options)
39
+
40
+ base = {:time => options[:time]}
41
+ unless options[:keep_identity]
42
+ base[:name] = 'git_dump'
43
+ base[:email] = "git_dump@#{GitDump.hostname}"
44
+ end
45
+
46
+ commit_sha = repo.commit(tree.sha, base.merge({
47
+ :message => options[:description],
48
+ }))
49
+
50
+ tag_name = repo.tag(commit_sha, name_parts(options), base.merge({
51
+ :message => options[:annotation],
52
+ }))
53
+
54
+ repo.gc(:auto => true)
55
+
56
+ Version.by_id(repo, tag_name)
57
+ end
58
+
59
+ def inspect
60
+ "#<#{self.class} tree=#{tree.inspect}>"
61
+ end
62
+
63
+ private
64
+
65
+ attr_reader :tree
66
+
67
+ def name_parts(options)
68
+ [
69
+ options[:time].dup.utc.strftime('%Y-%m-%d_%H-%M-%S'),
70
+ GitDump.hostname,
71
+ Array(options[:tags]).join(','),
72
+ GitDump.uuid,
73
+ ]
74
+ end
75
+ end
76
+ end
77
+ end
data/script/benchmark ADDED
@@ -0,0 +1,81 @@
1
+ #!/usr/bin/env ruby
2
+ # encoding: UTF-8
3
+
4
+ require 'rubygems'
5
+ require 'bundler/setup'
6
+
7
+ require 'git_dump'
8
+ require 'benchmark'
9
+ require 'tmpdir'
10
+
11
+ def measure(label, number = nil, &block)
12
+ label = "#{label} x#{number}" if number
13
+ label = "#{label}:"
14
+
15
+ result = nil
16
+ print label.ljust(30)
17
+ times = Benchmark.measure do
18
+ (number || 1).times do |n|
19
+ result = block.call(n)
20
+ end
21
+ end
22
+ puts " #{times}"
23
+ result
24
+ end
25
+
26
+ puts "Using #{defined?(Rugged) ? 'libgit2' : 'git commands'}:"
27
+
28
+ urandom = File.open('/dev/urandom')
29
+
30
+ Dir.mktmpdir do |dir|
31
+ dump = measure 'init' do
32
+ GitDump.new(File.join(dir, 'bm.git'), :create => true)
33
+ end
34
+
35
+ builder = dump.new_version
36
+
37
+ [
38
+ ['1K', 1_000, 1024],
39
+ ['10M', 10, 10 * 1024 * 1024],
40
+ ].each do |label, times, size|
41
+ datas = Array.new(times) do
42
+ urandom.read(size)
43
+ end.each
44
+ measure "store #{label} strings", times do |n|
45
+ path = format('data%s%08d', label, n).scan(/.{1,2}/).join('/')
46
+ data = datas.next
47
+ builder.store(path, data)
48
+ end
49
+
50
+ files = Array.new(times) do |i|
51
+ path = File.join(dir, "file#{i}")
52
+ File.open(path, 'w') do |f|
53
+ f.write(urandom.read(size))
54
+ end
55
+ path
56
+ end.each
57
+ measure "store #{label} files", times do |n|
58
+ path = format('file%s%08d', label, n).scan(/.{1,2}/).join('/')
59
+ file = files.next
60
+ builder.store_from(path, file)
61
+ end
62
+ end
63
+
64
+ measure 'commit', 10 do
65
+ builder.commit
66
+ end
67
+
68
+ measure 'read versions', 100 do
69
+ dump.versions
70
+ end
71
+
72
+ entries = dump.versions.first.each_recursive.to_a
73
+
74
+ measure 'read entries', entries.length do |n|
75
+ entries[n].read
76
+ end
77
+
78
+ measure 'read entries to disk', entries.length do |n|
79
+ entries[n].write_to(File.join(dir, 'out'))
80
+ end
81
+ end
@@ -0,0 +1,392 @@
1
+ require 'spec_helper'
2
+ require 'git_dump'
3
+ require 'tmpdir'
4
+
5
+ describe GitDump do
6
+ around do |example|
7
+ Dir.mktmpdir do |dir|
8
+ @tmp_dir = dir
9
+ example.run
10
+ end
11
+ end
12
+ let(:tmp_dir){ @tmp_dir }
13
+
14
+ DATAS = {
15
+ :text => "\r\n\r\nline\nline\rline\r\nline\n\rline\r\n\r\n",
16
+ :binary => 256.times.sort_by{ rand }.pack('C*'),
17
+ }
18
+
19
+ describe :new do
20
+ let(:path){ File.join(tmp_dir, 'dump') }
21
+
22
+ it 'initializes with bare git repo' do
23
+ system('git', 'init', '-q', '--bare', path)
24
+ expect(GitDump.new(path).path).to eq(path)
25
+ end
26
+
27
+ it 'initializes with full git repo' do
28
+ system('git', 'init', '-q', path)
29
+ expect(GitDump.new(path).path).to eq(path)
30
+ end
31
+
32
+ it 'creates bare git repo' do
33
+ dump = GitDump.new path, :create => true
34
+ Dir.chdir path do
35
+ expect(`git rev-parse --git-dir`.strip).to eq('.')
36
+ expect(`git rev-parse --is-bare-repository`.strip).to eq('true')
37
+ end
38
+ expect(dump.path).to eq(path)
39
+ end
40
+
41
+ it 'creates full git repo' do
42
+ dump = GitDump.new path, :create => :non_bare
43
+ Dir.chdir path do
44
+ expect(`git rev-parse --git-dir`.strip).to eq('.git')
45
+ expect(`git rev-parse --is-bare-repository`.strip).to eq('false')
46
+ end
47
+ expect(dump.path).to eq(path)
48
+ end
49
+
50
+ it 'raises if dump does not exist and not asked to create' do
51
+ expect{ GitDump.new path }.to raise_error GitDump::Repo::InitException
52
+ end
53
+
54
+ it 'raises if path is a file' do
55
+ File.open(path, 'w'){}
56
+ expect{ GitDump.new path }.to raise_error GitDump::Repo::InitException
57
+ end
58
+
59
+ it 'raises if dump is a directory but not a git repository' do
60
+ Dir.mkdir(path)
61
+ expect{ GitDump.new path }.to raise_error GitDump::Repo::InitException
62
+ end
63
+ end
64
+
65
+ context do
66
+ let(:dump){ GitDump.new File.join(tmp_dir, 'dump'), :create => true }
67
+
68
+ it 'returns empty list for empty repo versions' do
69
+ expect(dump.versions).to be_empty
70
+ end
71
+
72
+ it 'creates version for every commit' do
73
+ builder = dump.new_version
74
+ 3.times{ builder.commit }
75
+
76
+ expect(dump.versions.length).to eq(3)
77
+ end
78
+
79
+ it 'builds id from time, hostname and uuid' do
80
+ allow(GitDump).to receive(:hostname).and_return('ivans')
81
+ time = Time.utc(2000, 10, 20, 12, 34, 56)
82
+
83
+ builder = dump.new_version
84
+ built = builder.commit(:time => time)
85
+
86
+ expect(built.id).to match(%r{
87
+ \A
88
+ 2000-10-20_12-34-56
89
+ /
90
+ ivans
91
+ /
92
+ (?i:
93
+ [0-9a-f]{8}-
94
+ [0-9a-f]{4}-
95
+ 4[0-9a-f]{3}-
96
+ [89ab][0-9a-f]{3}-
97
+ [0-9a-f]{12}
98
+ )
99
+ \z
100
+ }x)
101
+ end
102
+
103
+ [
104
+ 'hello,world,foo,bar_',
105
+ %w[hello world foo bar_],
106
+ 'hello,world,foo,bar!@#$%^&*()',
107
+ ].each do |tags|
108
+ it 'puts tags in name' do
109
+ builder = dump.new_version
110
+ built = builder.commit(:tags => tags)
111
+
112
+ expect(built.id.split('/')).to include('hello,world,foo,bar_')
113
+ end
114
+ end
115
+
116
+ it 'sets and reads version time' do
117
+ time = Time.parse('2000-10-20 12:34:56')
118
+
119
+ builder = dump.new_version
120
+ built = builder.commit(:time => time)
121
+
122
+ expect(built.time).to eq(time)
123
+ expect(dump.versions.first.time).to eq(time)
124
+ end
125
+
126
+ it 'reads commit time' do
127
+ builder = dump.new_version
128
+ from = Time.at(Time.now.to_i) # round down to second
129
+ built = builder.commit(:time => Time.parse('2000-10-20 12:34:56'))
130
+ to = Time.now
131
+
132
+ expect(built.commit_time).to be_between(from, to)
133
+ expect(dump.versions.first.commit_time).to be_between(from, to)
134
+ end
135
+
136
+ it 'sets and reads version annotation' do
137
+ message = DATAS[:text] + File.read(__FILE__)
138
+
139
+ builder = dump.new_version
140
+ built = builder.commit(:annotation => message)
141
+
142
+ expect(built.annotation).to eq(message)
143
+ expect(dump.versions.first.annotation).to eq(message)
144
+ end
145
+
146
+ it 'sets and reads version description' do
147
+ message = DATAS[:text] + File.read(__FILE__)
148
+
149
+ builder = dump.new_version
150
+ built = builder.commit(:description => message)
151
+
152
+ expect(built.description).to eq(message)
153
+ expect(dump.versions.first.description).to eq(message)
154
+ end
155
+
156
+ it 'creates and reads version' do
157
+ builder = dump.new_version
158
+ builder['string/x'] = 'test a'
159
+ builder.store('stringio/x', StringIO.new('test b'), 0644)
160
+ builder.store('io/x', File.open(__FILE__), 0755)
161
+ builder.store_from('path/x', __FILE__)
162
+ built = builder.commit
163
+
164
+ reinit_dump = GitDump.new(dump.path)
165
+
166
+ expect(reinit_dump.versions.length).to eq(1)
167
+
168
+ version = reinit_dump.versions.first
169
+
170
+ expect(version.id).to eq(built.id)
171
+
172
+ expect(version['string/x'].read).to eq('test a')
173
+ expect(version['stringio/x'].read).to eq('test b')
174
+ expect(version['io/x'].read).to eq(File.read(__FILE__))
175
+ expect(version['path/x'].read).to eq(File.read(__FILE__))
176
+ expect(version['should/not/be/there']).to be_nil
177
+ end
178
+
179
+ it 'removes version' do
180
+ builder = dump.new_version
181
+ builder['a'] = 'b'
182
+ built = builder.commit
183
+
184
+ expect(dump.versions.length).to eq(1)
185
+
186
+ built.remove
187
+
188
+ expect(dump.versions).to be_empty
189
+ end
190
+
191
+ it 'returns path and name for trees and entries' do
192
+ builder = dump.new_version
193
+ builder['a/b/c'] = 'test'
194
+
195
+ %w[
196
+ a
197
+ a/b
198
+ a/b/c
199
+ ].each do |path|
200
+ expect(builder[path].path).to eq(path)
201
+ expect(builder[path].name).to eq(path.split('/').last)
202
+ end
203
+ end
204
+
205
+ it 'cleans paths' do
206
+ builder = dump.new_version
207
+ builder['//aa//fa//'] = 'test a'
208
+
209
+ expect(builder['//aa//fa//'].read).to eq('test a')
210
+ expect(builder['aa/fa//'].read).to eq('test a')
211
+ expect(builder['aa/fa'].read).to eq('test a')
212
+ end
213
+
214
+ it 'replaces tree branches' do
215
+ builder = dump.new_version
216
+ builder['a/a/a/a'] = 'test a'
217
+ builder['a/a/a/b'] = 'test b'
218
+ builder['a/a'] = 'hello'
219
+
220
+ expect(builder['a/a/a/a']).to be_nil
221
+ expect(builder['a/a/a/b']).to be_nil
222
+ expect(builder['a/a'].read).to eq('hello')
223
+ end
224
+
225
+ it 'removes entries and trees' do
226
+ builder = dump.new_version
227
+ builder['a/a/a'] = 'test a'
228
+ builder['a/a/b'] = 'test b'
229
+ builder['b/a'] = 'test c'
230
+ builder['b/b'] = 'test d'
231
+
232
+ builder['a/a'] = nil
233
+ builder['b/a'] = nil
234
+
235
+ version = builder.commit
236
+
237
+ expect(version.each.map(&:path)).to match_array(%w[a b])
238
+
239
+ expect(version['a'].each.map(&:path)).to be_empty
240
+
241
+ expect(version['b'].each.map(&:path)).to match_array(%w[b/b])
242
+ end
243
+
244
+ DATAS.each do |type, data|
245
+ it "does not change #{type} data" do
246
+ builder = dump.new_version
247
+ builder['a'] = data
248
+
249
+ expect(builder['a'].read).to eq(data)
250
+ end
251
+
252
+ it "does not change #{type} data read from file" do
253
+ path = File.join(tmp_dir, 'file.txt')
254
+ File.open(path, 'wb') do |f|
255
+ f.write(data)
256
+ end
257
+
258
+ builder = dump.new_version
259
+ builder.store_from('a', path)
260
+
261
+ expect(builder['a'].read).to eq(data)
262
+ end
263
+ end
264
+
265
+ describe :traversing do
266
+ let(:version) do
267
+ builder = dump.new_version
268
+ builder['c/c/c'] = 'c\c\c'
269
+ builder['a'] = 'a'
270
+ builder['b/a'] = 'b\a'
271
+ builder['b/b'] = 'b\b'
272
+ builder
273
+ end
274
+
275
+ def recursive_path_n_read(o)
276
+ o.each_recursive.map do |entry|
277
+ [entry.path, entry.read]
278
+ end
279
+ end
280
+
281
+ it 'traverses entries recursively' do
282
+ expect(recursive_path_n_read(version)).to match_array([
283
+ %w[a a],
284
+ %w[b/a b\a],
285
+ %w[b/b b\b],
286
+ %w[c/c/c c\c\c],
287
+ ])
288
+
289
+ expect(recursive_path_n_read(version['b'])).to match_array([
290
+ %w[b/a b\a],
291
+ %w[b/b b\b],
292
+ ])
293
+
294
+ expect(recursive_path_n_read(version['c'])).to match_array([
295
+ %w[c/c/c c\c\c],
296
+ ])
297
+
298
+ expect(recursive_path_n_read(version['c/c'])).to match_array([
299
+ %w[c/c/c c\c\c],
300
+ ])
301
+ end
302
+
303
+ it 'traverses level' do
304
+ expect(version.each.map(&:path)).to match_array(%w[a b c])
305
+
306
+ expect(version['b'].each.map(&:path)).to match_array(%w[b/a b/b])
307
+
308
+ expect(version['c'].each.map(&:path)).to eq(%w[c/c])
309
+
310
+ expect(version['c/c'].each.map(&:path)).to eq(%w[c/c/c])
311
+ end
312
+ end
313
+
314
+ describe :write_to do
315
+ DATAS.each do |type, data|
316
+ it "writes back #{type} data" do
317
+ builder = dump.new_version
318
+ builder['a'] = data
319
+
320
+ path = File.join(tmp_dir, 'file')
321
+ builder['a'].write_to(path)
322
+
323
+ expect(File.open(path, 'rb', &:read)).to eq(data)
324
+ end
325
+ end
326
+
327
+ [0644, 0755].each do |mode|
328
+ it "sets mode to #{mode.to_s(8)}" do
329
+ builder = dump.new_version
330
+ builder.store('a', 'test', mode)
331
+
332
+ path = File.join(tmp_dir, 'file')
333
+ builder['a'].write_to(path)
334
+
335
+ expect(File.stat(path).mode & 0777).to eq(mode)
336
+ end
337
+
338
+ it "fixes mode to #{mode.to_s(8)}" do
339
+ builder = dump.new_version
340
+ builder.store('a', 'test', mode & 0100)
341
+
342
+ path = File.join(tmp_dir, 'file')
343
+ builder['a'].write_to(path)
344
+
345
+ expect(File.stat(path).mode & 0777).to eq(mode)
346
+ end
347
+ end
348
+ end
349
+
350
+ it 'gets remote version ids' do
351
+ builder = dump.new_version
352
+ 3.times{ builder.commit }
353
+
354
+ expect(GitDump.remote_version_ids(dump.path)).
355
+ to eq(dump.versions.map(&:id))
356
+ end
357
+
358
+ describe :exchange do
359
+ let(:other_dump) do
360
+ GitDump.new File.join(tmp_dir, 'other'), :create => true
361
+ end
362
+
363
+ let(:built) do
364
+ builder = dump.new_version
365
+ builder['a'] = 'b'
366
+ builder.commit
367
+ end
368
+
369
+ def check_received_version
370
+ expect(other_dump.versions.length).to eq(1)
371
+
372
+ version = other_dump.versions.first
373
+ expect(version.id).to eq(built.id)
374
+ expect(version.each_recursive.map do |entry|
375
+ [entry.path, entry.read]
376
+ end).to eq([%w[a b]])
377
+ end
378
+
379
+ it 'pushes version' do
380
+ built.push(other_dump.path)
381
+
382
+ check_received_version
383
+ end
384
+
385
+ it 'fetches version' do
386
+ other_dump.fetch(dump.path, built.id)
387
+
388
+ check_received_version
389
+ end
390
+ end
391
+ end
392
+ end