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