eternity 0.0.1

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,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