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.
- checksums.yaml +7 -0
- data/.coveralls.yml +2 -0
- data/.gitignore +22 -0
- data/.ruby-gemset +1 -0
- data/.ruby-version +1 -0
- data/.travis.yml +9 -0
- data/Gemfile +6 -0
- data/LICENSE.txt +22 -0
- data/README.md +35 -0
- data/Rakefile +18 -0
- data/eternity.gemspec +39 -0
- data/lib/eternity.rb +64 -0
- data/lib/eternity/blob.rb +94 -0
- data/lib/eternity/branch.rb +34 -0
- data/lib/eternity/collection_index.rb +47 -0
- data/lib/eternity/collection_tracker.rb +55 -0
- data/lib/eternity/commit.rb +151 -0
- data/lib/eternity/conflict_resolver.rb +40 -0
- data/lib/eternity/delta.rb +45 -0
- data/lib/eternity/index.rb +29 -0
- data/lib/eternity/log.rb +22 -0
- data/lib/eternity/object_tracker.rb +57 -0
- data/lib/eternity/patch.rb +129 -0
- data/lib/eternity/repository.rb +235 -0
- data/lib/eternity/track_flatter.rb +57 -0
- data/lib/eternity/tracker.rb +38 -0
- data/lib/eternity/version.rb +3 -0
- data/spec/blob_spec.rb +83 -0
- data/spec/branch_spec.rb +71 -0
- data/spec/checkout_spec.rb +110 -0
- data/spec/commit_spec.rb +104 -0
- data/spec/coverage_helper.rb +8 -0
- data/spec/delta_spec.rb +110 -0
- data/spec/index_spec.rb +76 -0
- data/spec/locking_spec.rb +122 -0
- data/spec/log_spec.rb +57 -0
- data/spec/minitest_helper.rb +79 -0
- data/spec/patch_spec.rb +213 -0
- data/spec/pull_spec.rb +292 -0
- data/spec/push_spec.rb +72 -0
- data/spec/repository_spec.rb +73 -0
- data/spec/revert_spec.rb +40 -0
- data/spec/tracker_spec.rb +143 -0
- metadata +270 -0
@@ -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
|
data/lib/eternity/log.rb
ADDED
@@ -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
|