eternity 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -0,0 +1,235 @@
1
+ module Eternity
2
+ class Repository
3
+
4
+ extend Log
5
+
6
+ attr_reader :name, :id, :branches
7
+
8
+ def initialize(name, options={})
9
+ @name = name.to_s
10
+ @id = Eternity.keyspace[:repository][@name]
11
+ @tracker = Tracker.new self
12
+ @current = Restruct::Hash.new redis: Eternity.redis, id: id[:current]
13
+ @branches = Restruct::Hash.new redis: Eternity.redis, id: id[:branches]
14
+ @locker = Locky.new @name, Eternity.locker_storage
15
+ @default_branch = options.fetch(:default_branch) { 'master' }.to_s
16
+ end
17
+
18
+ def [](collection)
19
+ tracker[collection]
20
+ end
21
+
22
+ def empty?
23
+ tracker.empty? && current.empty? && branches.empty?
24
+ end
25
+
26
+ def changes?
27
+ !tracker.empty?
28
+ end
29
+
30
+ def changes_count
31
+ tracker.count
32
+ end
33
+
34
+ def delta
35
+ tracker.flatten
36
+ end
37
+
38
+ def delta=(delta)
39
+ tracker.clear
40
+ delta.each do |collection, changes|
41
+ changes.each do |id, change|
42
+ args = [id, change['data']].compact
43
+ self[collection].send(change['action'], *args)
44
+ end
45
+ end
46
+ end
47
+
48
+ def current_commit?
49
+ current.key? :commit
50
+ end
51
+
52
+ def current_commit
53
+ Commit.new current[:commit]
54
+ end
55
+
56
+ def current_branch
57
+ current[:branch] || @default_branch
58
+ end
59
+
60
+ def commit(options)
61
+ raise 'Nothing to commit' unless changes?
62
+
63
+ locker.lock! :commit do
64
+ commit! message: options.fetch(:message),
65
+ author: options.fetch(:author),
66
+ time: options.fetch(:time) { Time.now }
67
+ end
68
+ end
69
+
70
+ def branch(name)
71
+ raise "Can't branch without commit" unless current_commit?
72
+ raise "Can't branch with uncommitted changes" if changes?
73
+
74
+ branches[name] = current_commit.id
75
+ end
76
+
77
+ def checkout(options)
78
+ raise "Can't checkout with uncommitted changes" if changes?
79
+
80
+ locker.lock! :checkout do
81
+ original_commit = current_commit
82
+
83
+ commit_id, branch = extract_commit_and_branch options
84
+
85
+ if commit_id
86
+ raise "Invalid commit #{commit_id}" unless Commit.exists? commit_id
87
+ current[:commit] = commit_id
88
+ branches[branch] = commit_id
89
+ else
90
+ current.delete :commit
91
+ branches.delete branch
92
+ end
93
+
94
+ current[:branch] = branch
95
+
96
+ Patch.diff(original_commit, current_commit).delta
97
+ end
98
+ end
99
+
100
+ def merge(options)
101
+ raise "Can't merge with uncommitted changes" if changes?
102
+
103
+ commit_id = extract_commit options
104
+
105
+ raise "Invalid commit #{commit_id}" unless Commit.exists? commit_id
106
+
107
+ merge! Commit.new(commit_id)
108
+ end
109
+
110
+ def push
111
+ raise 'Push rejected (non fast forward)' if current_commit != Branch[current_branch] && !current_commit.fast_forward?(Branch[current_branch])
112
+ push!
113
+ end
114
+
115
+ def push!
116
+ raise "Can't push without commit" unless current_commit?
117
+ Branch[current_branch] = current_commit.id
118
+ end
119
+
120
+ def pull
121
+ raise "Can't pull with uncommitted changes" if changes?
122
+ raise "Branch not found: #{current_branch}" unless Branch.exists? current_branch
123
+
124
+ target_commit = Branch[current_branch]
125
+
126
+ if current_commit == target_commit || current_commit.fast_forward?(target_commit)
127
+ {}
128
+ elsif target_commit.fast_forward?(current_commit)
129
+ checkout commit: target_commit.id
130
+ else
131
+ merge! target_commit
132
+ end
133
+ end
134
+
135
+ def revert
136
+ locker.lock! :revert do
137
+ Delta.revert(delta, current_commit).tap { tracker.revert }
138
+ end
139
+ end
140
+
141
+ def log
142
+ current_commit? ? ([current_commit] + current_commit.history) : []
143
+ end
144
+
145
+ def destroy
146
+ tracker.destroy
147
+ current.destroy
148
+ branches.destroy
149
+ end
150
+
151
+ def to_h
152
+ {
153
+ 'current' => current.to_h,
154
+ 'branches' => branches.to_h,
155
+ 'delta' => delta
156
+ }
157
+ end
158
+ alias_method :dump, :to_h
159
+
160
+ def restore(dump)
161
+ current.merge! dump['current']
162
+ branches.merge! dump['branches']
163
+ self.delta = dump['delta']
164
+ end
165
+
166
+ [:delta, :delta=, :commit, :pull, :push, :checkout, :merge, :revert, :destroy, :dump].each { |m| log m }
167
+
168
+ private
169
+
170
+ attr_reader :tracker, :current, :locker
171
+
172
+ def commit!(options)
173
+ changes = delta
174
+ options[:parents] ||= [current_commit.id]
175
+ options[:delta] ||= write_delta changes
176
+ options[:index] ||= write_index changes
177
+
178
+ Commit.create(options).tap do |commit|
179
+ current[:commit] = commit.id
180
+ current[:branch] = current_branch
181
+ branches[current_branch] = commit.id
182
+ tracker.clear
183
+ end
184
+ end
185
+
186
+ def merge!(target_commit)
187
+ locker.lock! :merge do
188
+ patch = Patch.merge current_commit, target_commit
189
+
190
+ raise 'Already merged' if patch.merged?
191
+
192
+ commit! message: "Merge #{target_commit.short_id} into #{current_commit.short_id}",
193
+ author: 'System',
194
+ parents: [current_commit.id, target_commit.id],
195
+ index: write_index(patch.delta),
196
+ base: patch.base_commit.id,
197
+ base_delta: Blob.write(:delta, patch.base_delta)
198
+
199
+ patch.delta
200
+ end
201
+ end
202
+
203
+ def write_index(delta)
204
+ current_commit.with_index do |index|
205
+ index.apply delta
206
+ index.write_blob
207
+ end
208
+ end
209
+
210
+ def write_delta(delta)
211
+ Blob.write :delta, delta
212
+ end
213
+
214
+ def extract_commit(options)
215
+ extract_commit_and_branch(options).first
216
+ end
217
+
218
+ def extract_commit_and_branch(options)
219
+ branch = options.fetch(:branch) { current_branch }
220
+
221
+ commit_id = options.fetch(:commit) do
222
+ if branches.key? branch
223
+ branches[branch]
224
+ elsif Branch.exists?(branch)
225
+ Branch[branch].id
226
+ else
227
+ raise "Invalid branch #{branch}"
228
+ end
229
+ end
230
+
231
+ [commit_id, branch]
232
+ end
233
+
234
+ end
235
+ end
@@ -0,0 +1,57 @@
1
+ module Eternity
2
+ class TrackFlatter
3
+ class << self
4
+
5
+ def flatten(changes)
6
+ send "flatten_#{changes.first['action']}_#{changes.last['action']}", changes
7
+ end
8
+
9
+ private
10
+
11
+ def flatten_insert_insert(changes)
12
+ expand changes.last
13
+ end
14
+
15
+ def flatten_insert_update(changes)
16
+ {'action' => INSERT, 'data' => expand(changes.last)['data']}
17
+ end
18
+
19
+ def flatten_insert_delete(changes)
20
+ nil
21
+ end
22
+
23
+ def flatten_update_insert(changes)
24
+ {'action' => UPDATE, 'data' => expand(changes.last)['data']}
25
+ end
26
+
27
+ def flatten_update_update(changes)
28
+ expand changes.last
29
+ end
30
+
31
+ def flatten_update_delete(changes)
32
+ expand changes.last
33
+ end
34
+
35
+ def flatten_delete_insert(changes)
36
+ {'action' => UPDATE, 'data' => expand(changes.last)['data']}
37
+ end
38
+
39
+ def flatten_delete_update(changes)
40
+ {'action' => UPDATE, 'data' => expand(changes.last)['data']}
41
+ end
42
+
43
+ def flatten_delete_delete(changes)
44
+ expand changes.last
45
+ end
46
+
47
+ def expand(change)
48
+ return change if change.key? 'data'
49
+ change.tap do |ch|
50
+ sha1 = ch.delete 'blob'
51
+ ch['data'] = Blob.read(:data, sha1) if sha1
52
+ end
53
+ end
54
+
55
+ end
56
+ end
57
+ end
@@ -0,0 +1,38 @@
1
+ module Eternity
2
+ class Tracker
3
+
4
+ Changes = Restruct::NestedHash.new CollectionTracker
5
+
6
+ extend Forwardable
7
+ def_delegators :changes, :[], :to_h, :empty?, :destroy
8
+
9
+ attr_reader :repository
10
+
11
+ def initialize(repository)
12
+ @repository = repository
13
+ @changes = Changes.new redis: Eternity.redis,
14
+ id: repository.id[:changes]
15
+ end
16
+
17
+ def count
18
+ changes.inject(0) do |sum, (collection, tracker)|
19
+ sum + tracker.count
20
+ end
21
+ end
22
+
23
+ alias_method :revert, :destroy
24
+ alias_method :clear, :destroy
25
+
26
+ def flatten
27
+ changes.each_with_object({}) do |(collection, tracker), hash|
28
+ collection_changes = tracker.flatten
29
+ hash[collection] = collection_changes unless collection_changes.empty?
30
+ end
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :changes
36
+
37
+ end
38
+ end
@@ -0,0 +1,3 @@
1
+ module Eternity
2
+ VERSION = '0.0.1'
3
+ end
data/spec/blob_spec.rb ADDED
@@ -0,0 +1,83 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Blob do
4
+
5
+ let(:data) { {'id' => 'AR', 'name' => 'Argentina'} }
6
+ let(:serialization) { MessagePack.pack data }
7
+ let(:sha1) { Digest::SHA1.hexdigest serialization }
8
+ let(:key) { Eternity.keyspace[:blob][:xyz][sha1] }
9
+ let(:filename) { File.join(Eternity.blob_path, 'xyz', sha1[0..1], sha1[2..-1]) }
10
+
11
+ def encode(text)
12
+ Base64.encode64 text
13
+ end
14
+
15
+ def decode(text)
16
+ Base64.decode64 text
17
+ end
18
+
19
+ def wait_and_read_file(filename)
20
+ Timeout.timeout(5) do
21
+ until File.exists?(filename) && File.size(filename) > 0
22
+ sleep 0.001
23
+ end
24
+ IO.read filename
25
+ end
26
+ rescue Timeout::Error
27
+ raise "File not found: #{filename}"
28
+ end
29
+
30
+ it 'Write in redis and file system' do
31
+ sha1 = Blob.write :xyz, data
32
+
33
+ redis_data = redis.call 'GET', key
34
+ file_data = wait_and_read_file filename
35
+
36
+ [redis_data, decode(file_data)].each { |d| MessagePack.unpack(d).must_equal data }
37
+ end
38
+
39
+ it 'Read from redis' do
40
+ redis.call 'SET', key, serialization
41
+
42
+ refute File.exists?(filename)
43
+ Blob.read(:xyz, sha1).must_equal data
44
+ end
45
+
46
+ it 'Read from file' do
47
+ FileUtils.mkpath File.dirname(filename)
48
+ File.write filename, encode(serialization)
49
+
50
+ redis.call('GET', key).must_be_nil
51
+ Blob.read(:xyz, sha1).must_equal data
52
+ end
53
+
54
+ it 'Read invalid sha1' do
55
+ error = proc { Blob.read :xyz, 'invalid_sha1' }.must_raise RuntimeError
56
+ error.message.must_equal 'Blob not found: xyz -> invalid_sha1'
57
+ end
58
+
59
+ it 'Count' do
60
+ Blob.count.must_equal 0
61
+
62
+ 3.times { |i| Blob.write :xyz, value: i }
63
+
64
+ Blob.count.must_equal 3
65
+ end
66
+
67
+ it 'Clear cache' do
68
+ 3.times { |i| Blob.write :xyz, value: i }
69
+
70
+ Blob.clear_cache
71
+
72
+ Blob.count.must_equal 0
73
+ end
74
+
75
+ it 'Normalize serialization' do
76
+ time = Time.now
77
+ data_1 = {key_1: 1, key_2: time}
78
+ data_2 = {key_2: time.utc.strftime(TIME_FORMAT), key_1: 1}
79
+
80
+ Blob.serialize(data_1).must_equal Blob.serialize(data_2)
81
+ end
82
+
83
+ end
@@ -0,0 +1,71 @@
1
+ require 'minitest_helper'
2
+
3
+ describe Repository, 'Branch' do
4
+
5
+ describe 'Global' do
6
+
7
+ let(:commit_id) { SecureRandom.uuid }
8
+
9
+ it 'Get/Set' do
10
+ Branch[:test].id.must_be_nil
11
+ Branch[:test] = commit_id
12
+ Branch[:test].id.must_equal commit_id
13
+ end
14
+
15
+ it 'Exists' do
16
+ refute Branch.exists?(:test)
17
+ Branch[:test] = commit_id
18
+ assert Branch.exists?(:test)
19
+ end
20
+
21
+ it 'Delete' do
22
+ Branch[:test] = commit_id
23
+ assert Branch.exists?(:test)
24
+ Branch.delete :test
25
+ refute Branch.exists?(:test)
26
+ end
27
+
28
+ it 'Names' do
29
+ branches = %w(test_1 test_2 test_3)
30
+ branches.each { |b| Branch[b] = commit_id }
31
+ Branch.names.must_equal branches
32
+ end
33
+
34
+ end
35
+
36
+ describe 'Local' do
37
+
38
+ let(:repository) { Repository.new :test }
39
+
40
+ it 'New' do
41
+ repository[:countries].insert 'AR', name: 'Argentina'
42
+ commit = repository.commit author: 'User', message: 'Commit message'
43
+
44
+ repository.current_commit.must_equal commit
45
+ repository.current_branch.must_equal 'master'
46
+ repository.branches.to_h.must_equal 'master' => commit.id
47
+
48
+ repository.branch :test_branch
49
+
50
+ repository.current_branch.must_equal 'master'
51
+ repository.branches.to_h.must_equal 'master' => commit.id,
52
+ 'test_branch' => commit.id
53
+ end
54
+
55
+ it 'Without commit' do
56
+ error = proc { repository.branch :test_branch }.must_raise RuntimeError
57
+ error.message.must_equal "Can't branch without commit"
58
+ end
59
+
60
+ it 'With uncommitted changes' do
61
+ repository[:countries].insert 'AR', name: 'Argentina'
62
+ commit_id = repository.commit author: 'User', message: 'Commit message'
63
+ repository[:countries].delete 'AR'
64
+
65
+ error = proc { repository.branch :test_branch }.must_raise RuntimeError
66
+ error.message.must_equal "Can't branch with uncommitted changes"
67
+ end
68
+
69
+ end
70
+
71
+ end