has_versions 0.3.0 → 0.4.0
Sign up to get free protection for your applications and to get access to all the features.
- data/.gitignore +1 -37
- data/Gemfile.lock +17 -45
- data/Rakefile +16 -16
- data/has_versions.gemspec +6 -6
- data/lib/has_versions/apply/simple.rb +1 -1
- data/lib/has_versions/attributes.rb +2 -26
- data/lib/has_versions/configuration.rb +3 -16
- data/lib/has_versions/diff/simple.rb +1 -1
- data/lib/has_versions/merge/always_conflicted.rb +9 -0
- data/lib/has_versions/merge/base.rb +22 -0
- data/lib/has_versions/merge/choose_first.rb +9 -0
- data/lib/has_versions/merge/diff3.rb +66 -0
- data/lib/has_versions/merge/fast_forward.rb +34 -0
- data/lib/has_versions/merge/merge_base.rb +75 -0
- data/lib/has_versions/merge/octopus.rb +42 -0
- data/lib/has_versions/merge/three_way.rb +53 -0
- data/lib/has_versions/merge.rb +16 -45
- data/lib/has_versions/orm/cassandra_object.rb +15 -0
- data/lib/has_versions/reset/simple.rb +2 -2
- data/lib/has_versions/version.rb +1 -1
- data/lib/has_versions/versioned.rb +1 -5
- data/lib/has_versions/versioning_key.rb +0 -1
- data/lib/has_versions.rb +10 -0
- data/spec/has_versions/apply_spec.rb +5 -5
- data/spec/has_versions/configuration_spec.rb +7 -7
- data/spec/has_versions/diff_spec.rb +4 -4
- data/spec/has_versions/merge/diff3_spec.rb +29 -0
- data/spec/has_versions/merge/merge_base_spec.rb +61 -0
- data/spec/has_versions/merge/octopus_spec.rb +74 -0
- data/spec/has_versions/merge/three_way_spec.rb +57 -0
- data/spec/has_versions/reset_spec.rb +1 -2
- data/spec/has_versions/stage_spec.rb +14 -15
- data/spec/has_versions/versioned_spec.rb +4 -4
- data/spec/has_versions/versioning_key_spec.rb +1 -1
- data/spec/spec_helper.rb +7 -8
- data/spec/support/matchers/be_a_uuid.rb +0 -1
- data/spec/support/version.rb +10 -0
- metadata +34 -69
- data/lib/has_versions/merge/stupid.rb +0 -44
- data/lib/has_versions/stage.rb +0 -121
- data/spec/has_versions/merge_spec.rb +0 -67
- data/spec/support/matchers/have_key.rb +0 -25
data/.gitignore
CHANGED
@@ -1,42 +1,6 @@
|
|
1
|
-
# simplecov generated
|
2
1
|
coverage
|
3
|
-
|
4
|
-
# rdoc generated
|
5
2
|
rdoc
|
6
|
-
|
7
|
-
# yard generated
|
8
3
|
doc
|
9
4
|
.yardoc
|
10
|
-
|
11
|
-
# bundler
|
12
5
|
.bundle
|
13
|
-
|
14
|
-
# jeweler generated
|
15
|
-
pkg
|
16
|
-
|
17
|
-
# Have editor/IDE/OS specific files you need to ignore? Consider using a global gitignore:
|
18
|
-
#
|
19
|
-
# * Create a file at ~/.gitignore
|
20
|
-
# * Include files you want ignored
|
21
|
-
# * Run: git config --global core.excludesfile ~/.gitignore
|
22
|
-
#
|
23
|
-
# After doing this, these files will be ignored in all your git projects,
|
24
|
-
# saving you from having to 'pollute' every project you touch with them
|
25
|
-
#
|
26
|
-
# Not sure what to needs to be ignored for particular editors/OSes? Here's some ideas to get you started. (Remember, remove the leading # of the line)
|
27
|
-
#
|
28
|
-
# For MacOS:
|
29
|
-
#
|
30
|
-
#.DS_Store
|
31
|
-
#
|
32
|
-
# For TextMate
|
33
|
-
#*.tmproj
|
34
|
-
#tmtags
|
35
|
-
#
|
36
|
-
# For emacs:
|
37
|
-
#*~
|
38
|
-
#\#*
|
39
|
-
#.\#*
|
40
|
-
#
|
41
|
-
# For vim:
|
42
|
-
#*.swp
|
6
|
+
Gemfile.lock
|
data/Gemfile.lock
CHANGED
@@ -1,63 +1,35 @@
|
|
1
1
|
PATH
|
2
2
|
remote: .
|
3
3
|
specs:
|
4
|
-
has_versions (0.
|
5
|
-
activesupport (~> 3)
|
4
|
+
has_versions (0.4.0)
|
5
|
+
activesupport (~> 3.0)
|
6
6
|
i18n
|
7
7
|
simple_uuid
|
8
8
|
|
9
9
|
GEM
|
10
10
|
remote: http://rubygems.org/
|
11
11
|
specs:
|
12
|
-
activesupport (3.0
|
13
|
-
|
14
|
-
|
15
|
-
|
12
|
+
activesupport (3.1.0)
|
13
|
+
multi_json (~> 1.0)
|
14
|
+
diff-lcs (1.1.3)
|
15
|
+
i18n (0.6.0)
|
16
|
+
multi_json (1.0.3)
|
17
|
+
rspec (2.6.0)
|
18
|
+
rspec-core (~> 2.6.0)
|
19
|
+
rspec-expectations (~> 2.6.0)
|
20
|
+
rspec-mocks (~> 2.6.0)
|
21
|
+
rspec-core (2.6.4)
|
22
|
+
rspec-expectations (2.6.0)
|
16
23
|
diff-lcs (~> 1.1.2)
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
diff-lcs (1.1.2)
|
21
|
-
gherkin (2.3.4)
|
22
|
-
json (~> 1.4.6)
|
23
|
-
i18n (0.5.0)
|
24
|
-
json (1.4.6)
|
25
|
-
reek (1.2.8)
|
26
|
-
ruby2ruby (~> 1.2)
|
27
|
-
ruby_parser (~> 2.0)
|
28
|
-
sexp_processor (~> 3.0)
|
29
|
-
roodi (2.1.0)
|
30
|
-
ruby_parser
|
31
|
-
rspec (2.5.0)
|
32
|
-
rspec-core (~> 2.5.0)
|
33
|
-
rspec-expectations (~> 2.5.0)
|
34
|
-
rspec-mocks (~> 2.5.0)
|
35
|
-
rspec-core (2.5.1)
|
36
|
-
rspec-expectations (2.5.0)
|
37
|
-
diff-lcs (~> 1.1.2)
|
38
|
-
rspec-mocks (2.5.0)
|
39
|
-
ruby2ruby (1.2.5)
|
40
|
-
ruby_parser (~> 2.0)
|
41
|
-
sexp_processor (~> 3.0)
|
42
|
-
ruby_parser (2.0.6)
|
43
|
-
sexp_processor (~> 3.0)
|
44
|
-
sexp_processor (3.0.5)
|
45
|
-
simple_uuid (0.1.1)
|
46
|
-
simplecov (0.4.1)
|
47
|
-
simplecov-html (~> 0.4.3)
|
48
|
-
simplecov-html (0.4.3)
|
49
|
-
term-ansicolor (1.0.5)
|
50
|
-
yard (0.6.5)
|
24
|
+
rspec-mocks (2.6.0)
|
25
|
+
simple_uuid (0.2.0)
|
26
|
+
yard (0.6.8)
|
51
27
|
|
52
28
|
PLATFORMS
|
53
29
|
ruby
|
54
30
|
|
55
31
|
DEPENDENCIES
|
56
32
|
bundler (~> 1.0.0)
|
57
|
-
cucumber
|
58
33
|
has_versions!
|
59
|
-
|
60
|
-
roodi (~> 2.1.0)
|
61
|
-
rspec (~> 2)
|
62
|
-
simplecov (>= 0.3.8)
|
34
|
+
rspec (~> 2.0)
|
63
35
|
yard (~> 0.6.0)
|
data/Rakefile
CHANGED
@@ -1,4 +1,4 @@
|
|
1
|
-
require 'bundler'
|
1
|
+
require 'bundler/setup'
|
2
2
|
Bundler::GemHelper.install_tasks
|
3
3
|
|
4
4
|
require 'rspec/core'
|
@@ -7,23 +7,23 @@ RSpec::Core::RakeTask.new(:spec) do |spec|
|
|
7
7
|
spec.pattern = FileList['spec/**/*_spec.rb']
|
8
8
|
end
|
9
9
|
|
10
|
-
require 'cucumber/rake/task'
|
11
|
-
Cucumber::Rake::Task.new(:features)
|
10
|
+
# require 'cucumber/rake/task'
|
11
|
+
# Cucumber::Rake::Task.new(:features)
|
12
12
|
|
13
|
-
require 'reek/rake/task'
|
14
|
-
Reek::Rake::Task.new do |t|
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
end
|
13
|
+
# require 'reek/rake/task'
|
14
|
+
# Reek::Rake::Task.new do |t|
|
15
|
+
# t.fail_on_error = true
|
16
|
+
# t.verbose = false
|
17
|
+
# t.source_files = 'lib/**/*.rb'
|
18
|
+
# end
|
19
19
|
|
20
|
-
require 'roodi'
|
21
|
-
require 'roodi_task'
|
22
|
-
RoodiTask.new do |t|
|
23
|
-
|
24
|
-
end
|
20
|
+
# require 'roodi'
|
21
|
+
# require 'roodi_task'
|
22
|
+
# RoodiTask.new do |t|
|
23
|
+
# t.verbose = false
|
24
|
+
# end
|
25
25
|
|
26
26
|
task :default => :spec
|
27
27
|
|
28
|
-
require 'yard'
|
29
|
-
YARD::Rake::YardocTask.new
|
28
|
+
# require 'yard'
|
29
|
+
# YARD::Rake::YardocTask.new
|
data/has_versions.gemspec
CHANGED
@@ -27,14 +27,14 @@ Gem::Specification.new do |s|
|
|
27
27
|
s.licenses = ["MIT"]
|
28
28
|
|
29
29
|
s.add_runtime_dependency("i18n", [">= 0"])
|
30
|
-
s.add_runtime_dependency("activesupport", ["~> 3"])
|
30
|
+
s.add_runtime_dependency("activesupport", ["~> 3.0"])
|
31
31
|
s.add_runtime_dependency("simple_uuid", [">= 0"])
|
32
|
-
s.add_development_dependency("rspec", ["~> 2"])
|
32
|
+
s.add_development_dependency("rspec", ["~> 2.0"])
|
33
33
|
s.add_development_dependency("yard", ["~> 0.6.0"])
|
34
|
-
s.add_development_dependency("cucumber", [">= 0"])
|
35
34
|
s.add_development_dependency("bundler", ["~> 1.0.0"])
|
36
|
-
s.add_development_dependency("
|
37
|
-
s.add_development_dependency("
|
38
|
-
s.add_development_dependency("
|
35
|
+
# s.add_development_dependency("cucumber", [">= 0"])
|
36
|
+
# s.add_development_dependency("simplecov", [">= 0.3.8"])
|
37
|
+
# s.add_development_dependency("reek", ["~> 1.2.8"])
|
38
|
+
# s.add_development_dependency("roodi", ["~> 2.1.0"])
|
39
39
|
end
|
40
40
|
|
@@ -14,7 +14,7 @@ module HasVersions
|
|
14
14
|
module Simple
|
15
15
|
def apply(*patches)
|
16
16
|
self.class.new.tap do |version| #TODO is new the right thing here? it doe scall initialize (allocate does not)
|
17
|
-
ours = snapshot.
|
17
|
+
ours = snapshot.dup #TEST is it necessary to deep dup the snapshot?
|
18
18
|
|
19
19
|
patches.each do |patch|
|
20
20
|
patch.each do |key, (a, b)|
|
@@ -1,34 +1,10 @@
|
|
1
1
|
module HasVersions
|
2
2
|
module Attributes
|
3
|
-
|
4
3
|
def versioned_attributes
|
5
|
-
versioning_configuration.attributes.inject(
|
6
|
-
|
7
|
-
memo[key] = versioning_encode_value(__send__(key), options)
|
4
|
+
versioning_configuration.attributes.inject({}) do |memo, attribute|
|
5
|
+
memo[attribute] = versioning_encode_value(attribute, send(attribute))
|
8
6
|
memo
|
9
7
|
end
|
10
8
|
end
|
11
|
-
|
12
|
-
private
|
13
|
-
|
14
|
-
def versioning_decode_value(value, options)
|
15
|
-
return value if value.nil?
|
16
|
-
|
17
|
-
if options[:expected] && !value.kind_of?(options[:expected]) && options[:decoder]
|
18
|
-
options[:decoder].call(value)
|
19
|
-
else
|
20
|
-
value
|
21
|
-
end
|
22
|
-
end
|
23
|
-
|
24
|
-
def versioning_encode_value(value, options)
|
25
|
-
return value if value.nil?
|
26
|
-
|
27
|
-
if options[:encoder]
|
28
|
-
options[:encoder].call(value)
|
29
|
-
else
|
30
|
-
value
|
31
|
-
end
|
32
|
-
end
|
33
9
|
end
|
34
10
|
end
|
@@ -2,27 +2,14 @@ module HasVersions
|
|
2
2
|
|
3
3
|
class Configuration
|
4
4
|
attr_accessor :attributes
|
5
|
-
attr_accessor :types
|
6
|
-
|
7
|
-
DEFAULT_TYPES = {
|
8
|
-
set: { expected: Set, decoder: ->(value) { Set.new(value) } },
|
9
|
-
time_with_zone: { expected: ActiveSupport::TimeWithZone, encoder: ->(value) { value.utc.xmlschema(6) }, decoder: ->(value) { Time.xmlschema(value).in_time_zone } }
|
10
|
-
}.freeze
|
11
5
|
|
12
6
|
def initialize(&block)
|
13
|
-
|
14
|
-
@types = DEFAULT_TYPES.with_indifferent_access
|
7
|
+
self.attributes = []
|
15
8
|
instance_eval(&block)
|
16
9
|
end
|
17
10
|
|
18
|
-
def
|
19
|
-
|
20
|
-
end
|
21
|
-
|
22
|
-
def attribute(name, options={})
|
23
|
-
options.merge!(@types[options[:type]]) if @types[options[:type]]
|
24
|
-
@attributes[name] = options.with_indifferent_access
|
11
|
+
def attribute(name)
|
12
|
+
attributes << name.to_s
|
25
13
|
end
|
26
14
|
end
|
27
|
-
|
28
15
|
end
|
@@ -1,7 +1,7 @@
|
|
1
1
|
module HasVersions
|
2
2
|
module Diff
|
3
3
|
# takes a version and returns a diff (patch) from self
|
4
|
-
# version must respond to #snapshot with an attributes hash
|
4
|
+
# version must respond to #snapshot with an attributes hash (or something that responds to [] and keys)
|
5
5
|
# diff format is:
|
6
6
|
# {
|
7
7
|
# attribute_name: [a, b]
|
@@ -0,0 +1,22 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
class Base
|
4
|
+
attr_accessor :versions, :options
|
5
|
+
attr_accessor :resolution, :conflicts
|
6
|
+
|
7
|
+
def initialize(versions, options={})
|
8
|
+
@versions = versions
|
9
|
+
@options = options
|
10
|
+
@conflicts = {}.with_indifferent_access
|
11
|
+
end
|
12
|
+
|
13
|
+
def merge
|
14
|
+
# implemented by subclasses
|
15
|
+
end
|
16
|
+
|
17
|
+
def conflicted?
|
18
|
+
!self.conflicts.empty?
|
19
|
+
end
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,66 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
module Diff3
|
4
|
+
|
5
|
+
# 3-way merge, extended to support more than one other
|
6
|
+
# base ours *theirs.uniq take
|
7
|
+
# A A [A] A (ours)
|
8
|
+
# A B [A] B (ours)
|
9
|
+
# A A [B] B (theirs)
|
10
|
+
# B A [A] A (ours)
|
11
|
+
# B A [A,B] conflict
|
12
|
+
# B A [C] conflict
|
13
|
+
#
|
14
|
+
# cases where values are missing:
|
15
|
+
# m A A A
|
16
|
+
# m B A conflict
|
17
|
+
# A m A A
|
18
|
+
# A m B B
|
19
|
+
# A A m A
|
20
|
+
# A B m B
|
21
|
+
# m m A A
|
22
|
+
# m A m A
|
23
|
+
#
|
24
|
+
# returns an array [clean, result]
|
25
|
+
# where clean is a boolean
|
26
|
+
# and result is an array in the conflicts case, or a single value in the clean case
|
27
|
+
|
28
|
+
def diff3(base, ours, *theirs)
|
29
|
+
theirs = theirs.uniq
|
30
|
+
|
31
|
+
# if there is more than one possible value, it is already a conflict
|
32
|
+
if theirs.size > 1
|
33
|
+
# add ours to conflicts if it is different from base
|
34
|
+
if ours != base && !theirs.include?(ours)
|
35
|
+
theirs << ours
|
36
|
+
end
|
37
|
+
return [false, theirs]
|
38
|
+
end
|
39
|
+
|
40
|
+
theirs = theirs.first
|
41
|
+
|
42
|
+
if ours == base && base == theirs
|
43
|
+
[true, ours]
|
44
|
+
elsif base == theirs
|
45
|
+
[true, ours]
|
46
|
+
elsif ours == base
|
47
|
+
[true, theirs]
|
48
|
+
elsif ours == theirs
|
49
|
+
[true, ours]
|
50
|
+
elsif ours == :_missing
|
51
|
+
[true, theirs]
|
52
|
+
elsif theirs == :_missing
|
53
|
+
[true, ours]
|
54
|
+
else
|
55
|
+
[false, [ours, theirs]]
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
private
|
60
|
+
|
61
|
+
def value_or_missing(snapshot, key)
|
62
|
+
snapshot.keys.include?(key) ? snapshot[key] : :_missing
|
63
|
+
end
|
64
|
+
end
|
65
|
+
end
|
66
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
class FastForward < Base
|
4
|
+
# Takes exactly two versions and resolves the merge using ancestry.
|
5
|
+
# One of the two versions must be an ancestor of the other.
|
6
|
+
#
|
7
|
+
# expects versions to define contains? which returns true if the given version
|
8
|
+
# is an ancestor of the receiver
|
9
|
+
# eg if v1 is an ancestor of v2, then
|
10
|
+
# v2.contains?(v1) #=> true
|
11
|
+
#
|
12
|
+
# raises MergeFailed if anything but two versions are given, or if neither version
|
13
|
+
# is an ancestor of the other.
|
14
|
+
|
15
|
+
def initialize(versions, options={})
|
16
|
+
super
|
17
|
+
if versions.size != 2
|
18
|
+
raise HasVersions::MergeFailed, "Cannot fast forward merge with #{versions.size} versions"
|
19
|
+
end
|
20
|
+
end
|
21
|
+
|
22
|
+
def merge
|
23
|
+
v1, v2 = versions
|
24
|
+
if v1.contains?(v2)
|
25
|
+
self.resolution = v1
|
26
|
+
elsif v2.contains?(v1)
|
27
|
+
self.resolution = v2
|
28
|
+
else
|
29
|
+
raise HasVersions::MergeFailed, "cannot fast forward if neither version is an ancestor"
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,75 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
module MergeBase
|
4
|
+
# Finds optimal merge bases for any number of versions
|
5
|
+
# Essentially a copy of git's algorithm
|
6
|
+
# Expects versions to respond to parents with an array of parent versions
|
7
|
+
def merge_bases(versions)
|
8
|
+
versions = versions.dup
|
9
|
+
one = versions.shift
|
10
|
+
twos = versions
|
11
|
+
|
12
|
+
bases = determine_merge_bases(one, twos)
|
13
|
+
if twos.include?(one)
|
14
|
+
return bases
|
15
|
+
end
|
16
|
+
|
17
|
+
if bases.size == 1
|
18
|
+
return bases
|
19
|
+
end
|
20
|
+
|
21
|
+
bases.each_with_index do |base1, i|
|
22
|
+
next if base1 == bases.last
|
23
|
+
|
24
|
+
bases.each_with_index do |base2, j|
|
25
|
+
next if base1.nil? || base2.nil?
|
26
|
+
sub_bases = determine_merge_bases(base1, [base2])
|
27
|
+
sub_bases.each do |sub_base|
|
28
|
+
bases[i] = nil if sub_base == base1
|
29
|
+
bases[j] = nil if sub_base == base2
|
30
|
+
end
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
bases.compact
|
35
|
+
end
|
36
|
+
|
37
|
+
private
|
38
|
+
|
39
|
+
def determine_merge_bases(one, twos)
|
40
|
+
stale = Set.new
|
41
|
+
parent1 = Set.new
|
42
|
+
parent2 = Set.new
|
43
|
+
result = Set.new
|
44
|
+
|
45
|
+
interesting = []
|
46
|
+
|
47
|
+
if twos.include?(one)
|
48
|
+
return [one]
|
49
|
+
end
|
50
|
+
|
51
|
+
parent1 << one
|
52
|
+
parent2 += twos
|
53
|
+
|
54
|
+
interesting << one
|
55
|
+
interesting += twos
|
56
|
+
|
57
|
+
while !(interesting - stale.to_a).empty?
|
58
|
+
candidate = interesting.shift
|
59
|
+
if parent1.include?(candidate) && parent2.include?(candidate) && !stale.include?(candidate)
|
60
|
+
result << candidate
|
61
|
+
end
|
62
|
+
|
63
|
+
candidate.parents.each do |parent|
|
64
|
+
stale << parent if stale.include?(candidate)
|
65
|
+
parent1 << parent if parent1.include?(candidate)
|
66
|
+
parent2 << parent if parent2.include?(candidate)
|
67
|
+
interesting << parent
|
68
|
+
end
|
69
|
+
end
|
70
|
+
|
71
|
+
result.to_a.reject { |r| stale.include?(r) }
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
# Merge >2 versions
|
4
|
+
# Works by doing a 3-way merge for first 2 versions, then merging the result with
|
5
|
+
# the next version for each version given.
|
6
|
+
# eg for 4 versions there are three merges: v1+v2, then (v1+v2)+v3, then ((v1+v2)+v3)+v4
|
7
|
+
# conflicts will be an accumulation of all conflicting values from each merge
|
8
|
+
class Octopus < Base
|
9
|
+
def initialize(versions, options={})
|
10
|
+
super
|
11
|
+
if versions.size <= 2
|
12
|
+
raise HasVersions::MergeFailed, "Cannot octopus merge with #{versions.size} versions, expect >2"
|
13
|
+
end
|
14
|
+
end
|
15
|
+
|
16
|
+
def merge
|
17
|
+
self.resolution = versions.inject(resolution) do |res, version|
|
18
|
+
if res
|
19
|
+
merge = ThreeWay.new([res, version], options)
|
20
|
+
merge.merge
|
21
|
+
add_conflicts(merge.conflicts)
|
22
|
+
res = merge.resolution
|
23
|
+
else
|
24
|
+
res = version
|
25
|
+
end
|
26
|
+
res
|
27
|
+
end
|
28
|
+
resolution.parents = versions
|
29
|
+
resolution
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
def add_conflicts(conflicts)
|
34
|
+
conflicts.each do |attribute, values|
|
35
|
+
self.conflicts[attribute] ||= []
|
36
|
+
self.conflicts[attribute] += values
|
37
|
+
self.conflicts[attribute].uniq!
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Merge
|
3
|
+
# Merges two versions using ancestry and 3-way diff
|
4
|
+
# expects versions to respond to parents= to set parents
|
5
|
+
# does not check ancestry when setting parents - use FastForward if one version
|
6
|
+
# is an ancestor of another or they are identical
|
7
|
+
class ThreeWay < Base
|
8
|
+
include MergeBase
|
9
|
+
include Diff3
|
10
|
+
|
11
|
+
def initialize(versions, options={})
|
12
|
+
super
|
13
|
+
if versions.size != 2
|
14
|
+
raise HasVersions::MergeFailed, "Cannot 3-way merge with #{versions.size} versions, expect 2"
|
15
|
+
end
|
16
|
+
end
|
17
|
+
|
18
|
+
# assume the first version is ours
|
19
|
+
def merge
|
20
|
+
version_class = options[:version_class] || versions.first.class
|
21
|
+
# find merge base or bases
|
22
|
+
bases = merge_bases(versions)
|
23
|
+
# raise if there is more than one base
|
24
|
+
# TODO could potentially just choose one (that's what git does)
|
25
|
+
raise MergeFailed, "More than one merge base" if bases.size > 1
|
26
|
+
|
27
|
+
base = bases.first.try(:snapshot) || {}.with_indifferent_access
|
28
|
+
ours = versions.first.snapshot
|
29
|
+
theirs = versions.last.snapshot
|
30
|
+
|
31
|
+
self.resolution = version_class.new
|
32
|
+
self.resolution.parents = versions
|
33
|
+
|
34
|
+
# get the full list of attributes in each version and the merge base
|
35
|
+
attributes = (versions.collect { |v| v.snapshot.keys }.flatten + base.keys).uniq
|
36
|
+
|
37
|
+
# do 3-way merge on each attribute
|
38
|
+
attributes.each do |key|
|
39
|
+
clean, result = diff3(value_or_missing(base, key), value_or_missing(ours, key), value_or_missing(theirs, key))
|
40
|
+
if clean
|
41
|
+
resolution.snapshot[key] = result
|
42
|
+
else
|
43
|
+
resolution.snapshot[key] = ours[key]
|
44
|
+
conflicts[key] = result
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
resolution
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
data/lib/has_versions/merge.rb
CHANGED
@@ -1,6 +1,8 @@
|
|
1
1
|
module HasVersions
|
2
2
|
|
3
|
-
class
|
3
|
+
class MergeFailed < VersioningError; end
|
4
|
+
|
5
|
+
class MergeConflict < MergeFailed
|
4
6
|
attr_accessor :conflicts
|
5
7
|
attr_accessor :result
|
6
8
|
|
@@ -13,49 +15,18 @@ module HasVersions
|
|
13
15
|
module Merge
|
14
16
|
extend ActiveSupport::Autoload
|
15
17
|
|
16
|
-
autoload :
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
# returns an array [clean, result]
|
30
|
-
# where clean is a boolean
|
31
|
-
# and result is an array in the conflicts case, or a single value in the clean case
|
32
|
-
|
33
|
-
def diff3(base, ours, *theirs)
|
34
|
-
theirs = theirs.uniq
|
35
|
-
|
36
|
-
# if there is more than one possible value, it is already a conflict
|
37
|
-
if theirs.size > 1
|
38
|
-
# add ours to conflicts if it is different from base
|
39
|
-
if ours != base && !theirs.include?(ours)
|
40
|
-
theirs << ours
|
41
|
-
end
|
42
|
-
return [false, theirs]
|
43
|
-
end
|
44
|
-
|
45
|
-
theirs = theirs.first
|
46
|
-
|
47
|
-
if ours == base && base == theirs
|
48
|
-
[true, ours]
|
49
|
-
elsif base == theirs
|
50
|
-
[true, ours]
|
51
|
-
elsif ours == base
|
52
|
-
[true, theirs]
|
53
|
-
elsif ours == theirs
|
54
|
-
[true, ours]
|
55
|
-
else
|
56
|
-
[false, [ours, theirs]]
|
57
|
-
end
|
58
|
-
end
|
59
|
-
end
|
18
|
+
autoload :Base
|
19
|
+
|
20
|
+
autoload :FastForward
|
21
|
+
autoload :ThreeWay
|
22
|
+
autoload :Octopus
|
23
|
+
|
24
|
+
# strategies for testing
|
25
|
+
autoload :ChooseFirst
|
26
|
+
autoload :AlwaysConflicted
|
27
|
+
|
28
|
+
autoload :Diff3
|
29
|
+
autoload :MergeBase
|
30
|
+
|
60
31
|
end
|
61
32
|
end
|
@@ -0,0 +1,15 @@
|
|
1
|
+
module HasVersions
|
2
|
+
module Orm
|
3
|
+
module CassandraObject
|
4
|
+
def versioning_encode_value(name, value)
|
5
|
+
return if value.nil?
|
6
|
+
attribute_definitions[name.to_sym].coder.encode(value)
|
7
|
+
end
|
8
|
+
|
9
|
+
def versioning_decode_value(name, value)
|
10
|
+
return if value.nil?
|
11
|
+
attribute_definitions[name.to_sym].coder.decode(value)
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|