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,151 @@
1
+ module Eternity
2
+ class Commit
3
+
4
+ attr_reader :id
5
+
6
+ def initialize(id)
7
+ @id = id
8
+ end
9
+
10
+ def short_id
11
+ id ? id[0,7] : nil
12
+ end
13
+
14
+ def time
15
+ Time.parse(data['time']) if data['time']
16
+ end
17
+
18
+ def author
19
+ data['author']
20
+ end
21
+
22
+ def message
23
+ data['message']
24
+ end
25
+
26
+ def parent_ids
27
+ data['parents'] || [nil]
28
+ end
29
+
30
+ def parents
31
+ parent_ids.map { |id| Commit.new id }
32
+ end
33
+
34
+ def with_index
35
+ index = data['index'] ? Index.read_blob(data['index']) : Index.new
36
+ yield index
37
+ ensure
38
+ index.destroy if index
39
+ end
40
+
41
+ def delta
42
+ data['delta'] ? Blob.read(:delta, data['delta']) : {}
43
+ end
44
+
45
+ def base
46
+ Commit.new data['base']
47
+ end
48
+
49
+ def base_delta
50
+ data['base_delta'] ? Blob.read(:delta, data['base_delta']) : {}
51
+ end
52
+
53
+ def history_times
54
+ data['history_times'] ? Blob.read(:history_times, data['history_times']) : {}
55
+ end
56
+
57
+ def history
58
+ history_times.sort_by { |id, time| time }
59
+ .map { |id, time| Commit.new id }
60
+ .reverse
61
+ end
62
+
63
+ def fast_forward?(commit)
64
+ return commit.nil? if first?
65
+ parent_ids.any? { |id| id == commit.id } || parents.map { |c| c.fast_forward?(commit) }.inject(:|)
66
+ end
67
+
68
+ def base_history_at(commit)
69
+ return [] unless commit
70
+ history = [base]
71
+ history += base.base_history_at commit unless base.id == commit.id
72
+ raise "History not include commit #{commit.id}" unless history.map(&:id).include? commit.id
73
+ history
74
+ end
75
+
76
+ def first?
77
+ parent_ids.compact.empty?
78
+ end
79
+
80
+ def merge?
81
+ parent_ids.count == 2
82
+ end
83
+
84
+ def nil?
85
+ id.nil?
86
+ end
87
+
88
+ def ==(commit)
89
+ commit.class == self.class &&
90
+ commit.id == id
91
+ end
92
+ alias_method :eql?, :==
93
+
94
+ def self.create(options)
95
+ raise 'Author must be present' if options[:author].to_s.strip.empty?
96
+ raise 'Message must be present' if options[:message].to_s.strip.empty?
97
+
98
+ history_times = options[:parents].compact.each_with_object({}) do |id, hash|
99
+ commit = Commit.new id
100
+ hash.merge! id => commit.time
101
+ hash.merge! commit.history_times
102
+ end
103
+
104
+ data = {
105
+ time: options.fetch(:time) { Time.now },
106
+ author: options.fetch(:author),
107
+ message: options.fetch(:message),
108
+ parents: options.fetch(:parents),
109
+ index: options.fetch(:index),
110
+ delta: options.fetch(:delta),
111
+ base: options[:parents].count == 2 ? options.fetch(:base) : options[:parents].first,
112
+ base_delta: options[:parents].count == 2 ? options.fetch(:base_delta) : options.fetch(:delta),
113
+ history_times: Blob.write(:history_times, history_times)
114
+ }
115
+
116
+ new Blob.write(:commit, data)
117
+ end
118
+
119
+ def self.base_of(commit_1, commit_2)
120
+ history_1 = [commit_1.id]
121
+ history_2 = [commit_2.id]
122
+
123
+ base_1 = commit_1
124
+ base_2 = commit_2
125
+
126
+ while (history_1 & history_2).empty?
127
+ base_1 = base_1.base if base_1
128
+ base_2 = base_2.base if base_2
129
+
130
+ history_1 << base_1.id if base_1
131
+ history_2 << base_2.id if base_2
132
+ end
133
+
134
+ Commit.new (history_1 & history_2).first
135
+ end
136
+
137
+ def self.exists?(id)
138
+ Blob.read :commit, id
139
+ true
140
+ rescue
141
+ false
142
+ end
143
+
144
+ private
145
+
146
+ def data
147
+ @data ||= id ? Blob.read(:commit, id) : {}
148
+ end
149
+
150
+ end
151
+ end
@@ -0,0 +1,40 @@
1
+ module Eternity
2
+ class ConflictResolver
3
+
4
+ Diff = Struct.new :added, :updated, :removed
5
+
6
+ attr_reader :current, :target, :base
7
+
8
+ def initialize(current, target, base={})
9
+ @current = current
10
+ @target = target
11
+ @base = base
12
+ end
13
+
14
+ def resolve
15
+ current_diff = diff current, base
16
+ target_diff = diff target, base
17
+ merge(target_diff, target, merge(current_diff, current, base))
18
+ end
19
+
20
+ def self.resolve(current, target, base={})
21
+ new(current, target, base).resolve
22
+ end
23
+
24
+ private
25
+
26
+ def diff(object, base)
27
+ Diff.new object.keys - base.keys,
28
+ base.keys.select { |k| base[k] != object[k] },
29
+ base.keys - object.keys
30
+ end
31
+
32
+ def merge(diff, object, base)
33
+ base.dup.tap do |result|
34
+ (diff.added + diff.updated).each { |k| result[k] = object[k] }
35
+ diff.removed.each { |k| result.delete k }
36
+ end
37
+ end
38
+
39
+ end
40
+ end
@@ -0,0 +1,45 @@
1
+ module Eternity
2
+ class Delta
3
+ class << self
4
+
5
+ def union(deltas)
6
+ deltas.each_with_object({}) do |delta, hash|
7
+ delta.each do |collection, elements|
8
+ hash[collection] ||= {}
9
+ elements.each do |id, change|
10
+ hash[collection][id] ||= []
11
+ hash[collection][id] << change
12
+ end
13
+ end
14
+ end
15
+ end
16
+
17
+ def merge(deltas)
18
+ union(deltas).each_with_object({}) do |(collection, elements), hash|
19
+ hash[collection] = {}
20
+ elements.each do |id, changes|
21
+ change = TrackFlatter.flatten changes
22
+ hash[collection][id] = TrackFlatter.flatten changes if change
23
+ end
24
+ end
25
+ end
26
+
27
+ def revert(delta, commit)
28
+ commit.with_index do |index|
29
+ delta.each_with_object({}) do |(collection, changes), hash|
30
+ hash[collection] = {}
31
+ changes.each do |id, change|
32
+ hash[collection][id] =
33
+ case change['action']
34
+ when INSERT then {'action' => DELETE}
35
+ when UPDATE then {'action' => UPDATE, 'data' => index[collection][id].data}
36
+ when DELETE then {'action' => INSERT, 'data' => index[collection][id].data}
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,29 @@
1
+ module Eternity
2
+ class Index < Restruct::NestedHash.new(CollectionIndex)
3
+
4
+ def initialize
5
+ super redis: Eternity.redis,
6
+ id: Eternity.keyspace[:index][SecureRandom.uuid]
7
+ end
8
+
9
+ def apply(delta)
10
+ delta.each do |collection, elements|
11
+ elements.each do |id, change|
12
+ args = [id, change['data']].compact
13
+ self[collection].send change['action'], *args
14
+ end
15
+ end
16
+ end
17
+
18
+ def write_blob
19
+ Blob.write :index, dump
20
+ end
21
+
22
+ def self.read_blob(sha1)
23
+ Index.new.tap do |index|
24
+ index.restore Blob.read :index, sha1
25
+ end
26
+ end
27
+
28
+ end
29
+ end
@@ -0,0 +1,22 @@
1
+ module Eternity
2
+ module Log
3
+
4
+ private
5
+
6
+ def log(method)
7
+ original_method = "__#{method}_without_log__"
8
+
9
+ alias_method original_method, method
10
+
11
+ define_method method do |*args, &block|
12
+ Eternity.logger.info(self.class.name) { "#{method} (Start)" }
13
+ result = send original_method, *args, &block
14
+ Eternity.logger.info(self.class.name) { "#{method} (End)" }
15
+ result
16
+ end
17
+
18
+ private original_method
19
+ end
20
+
21
+ end
22
+ end
@@ -0,0 +1,57 @@
1
+ module Eternity
2
+ class ObjectTracker
3
+
4
+ extend Forwardable
5
+ def_delegators :changes, :to_a, :to_primitive, :count, :each, :destroy
6
+
7
+ def initialize(options)
8
+ @changes = Restruct::MarshalArray.new options
9
+ end
10
+
11
+ def insert(data)
12
+ track INSERT, data
13
+ end
14
+
15
+ def update(data)
16
+ track UPDATE, data
17
+ end
18
+
19
+ def delete
20
+ track DELETE
21
+ end
22
+
23
+ def revert
24
+ locker.lock! :revert do
25
+ changes.destroy
26
+ end
27
+ end
28
+
29
+ def flatten
30
+ TrackFlatter.flatten changes
31
+ end
32
+
33
+ private
34
+
35
+ attr_reader :changes
36
+
37
+ def track(action, data=nil)
38
+ locker.lock :track do
39
+ change = {'action' => action}
40
+ change['blob'] = Blob.write(:data, data) if data
41
+
42
+ Eternity.logger.debug(self.class) { "#{changes.id} - #{change} - #{data}" }
43
+
44
+ changes << change
45
+ end
46
+ end
47
+
48
+ def locker
49
+ Locky.new repository_name, Eternity.locker_storage
50
+ end
51
+
52
+ def repository_name
53
+ changes.id.sections.reverse[3]
54
+ end
55
+
56
+ end
57
+ end
@@ -0,0 +1,129 @@
1
+ module Eternity
2
+ module Patch
3
+
4
+ def self.merge(current_commit, target_commit)
5
+ Merge.new current_commit, target_commit
6
+ end
7
+
8
+ def self.diff(current_commit, target_commit)
9
+ Diff.new current_commit, target_commit
10
+ end
11
+
12
+ module Common
13
+
14
+ attr_reader :current_commit, :target_commit
15
+
16
+ def initialize(current_commit, target_commit)
17
+ @current_commit = current_commit
18
+ @target_commit = target_commit
19
+ end
20
+
21
+ def base_commit
22
+ @base_commit ||= Commit.base_of current_commit, target_commit
23
+ end
24
+
25
+ def delta
26
+ @delta ||= TransparentProxy.new { calculate_delta }
27
+ end
28
+
29
+ private
30
+
31
+ def current_delta
32
+ @current_delta ||= base_delta_of current_commit, base_commit
33
+ end
34
+
35
+ def target_delta
36
+ @target_delta ||= base_delta_of target_commit, base_commit
37
+ end
38
+
39
+ def base_delta_of(commit, base)
40
+ return {} if commit == base
41
+ history = [commit] + commit.base_history_at(base)[0..-2]
42
+ Delta.merge history.reverse.map(&:base_delta)
43
+ end
44
+
45
+ end
46
+
47
+
48
+ class Merge
49
+
50
+ extend Log
51
+ include Common
52
+
53
+ def base_delta
54
+ @base_delta ||= merged? ? {} : Delta.merge([current_delta, delta])
55
+ end
56
+
57
+ def merged?
58
+ @merged ||= current_commit == target_commit ||
59
+ target_commit.fast_forward?(current_commit) ||
60
+ current_commit.fast_forward?(target_commit)
61
+ end
62
+
63
+ private
64
+
65
+ def calculate_delta
66
+ return {} if merged?
67
+
68
+ base_commit.with_index do |base_index|
69
+ target_delta.each_with_object({}) do |(collection, elements), hash|
70
+ hash[collection] = {}
71
+
72
+ elements.each do |id, change|
73
+ if change['action'] == INSERT && current_action_for(collection, id) == INSERT
74
+ data = ConflictResolver.resolve current_delta[collection][id]['data'],
75
+ change['data']
76
+ change = {'action' => UPDATE, 'data' => data}
77
+
78
+ elsif change['action'] == UPDATE
79
+ if current_action_for(collection, id) == UPDATE
80
+ data = ConflictResolver.resolve current_delta[collection][id]['data'],
81
+ change['data'],
82
+ base_index[collection][id].data
83
+ change = change.merge 'data' => data
84
+ elsif current_action_for(collection, id) == DELETE
85
+ change = {'action' => INSERT, 'data' => change['data']}
86
+ end
87
+
88
+ elsif change['action'] == DELETE && current_action_for(collection, id) == DELETE
89
+ change = nil
90
+ end
91
+
92
+ hash[collection][id] = change if change
93
+ end
94
+
95
+ hash.delete collection if hash[collection].empty?
96
+ end
97
+ end
98
+ end
99
+
100
+ def has_current_changes_for?(collection, id)
101
+ current_delta.key?(collection) && current_delta[collection].key?(id)
102
+ end
103
+
104
+ def current_action_for(collection, id)
105
+ current_delta[collection][id]['action'] if has_current_changes_for? collection, id
106
+ end
107
+
108
+ log :calculate_delta
109
+
110
+ end
111
+
112
+
113
+ class Diff
114
+
115
+ extend Log
116
+ include Common
117
+
118
+ private
119
+
120
+ def calculate_delta
121
+ Delta.merge [Delta.revert(current_delta, base_commit), target_delta]
122
+ end
123
+
124
+ log :calculate_delta
125
+
126
+ end
127
+
128
+ end
129
+ end