has_versions 0.3.0 → 0.4.0
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.
- 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
|