git_dump 0.1.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.
@@ -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