eternity 0.0.1

Sign up to get free protection for your applications and to get access to all the features.
@@ -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