archimate-diff 0.1.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.
- checksums.yaml +7 -0
- data/.gitignore +22 -0
- data/.projectile +18 -0
- data/.rubocop.yml +13 -0
- data/.travis.yml +4 -0
- data/.yardocs +15 -0
- data/Gemfile +5 -0
- data/Guardfile +27 -0
- data/LICENSE +201 -0
- data/README.md +94 -0
- data/Rakefile +29 -0
- data/archimate-diff.gemspec +51 -0
- data/exe/archidiff +7 -0
- data/exe/archidiff-summary +7 -0
- data/exe/archimerge +7 -0
- data/exe/fmtxml +7 -0
- data/lib/archimate-diff.rb +33 -0
- data/lib/archimate/diff/archimate_array_reference.rb +113 -0
- data/lib/archimate/diff/archimate_identified_node_reference.rb +41 -0
- data/lib/archimate/diff/archimate_node_attribute_reference.rb +70 -0
- data/lib/archimate/diff/archimate_node_reference.rb +80 -0
- data/lib/archimate/diff/change.rb +49 -0
- data/lib/archimate/diff/cli/conflict_resolver.rb +41 -0
- data/lib/archimate/diff/cli/diff.rb +33 -0
- data/lib/archimate/diff/cli/diff_summary.rb +103 -0
- data/lib/archimate/diff/cli/merge.rb +51 -0
- data/lib/archimate/diff/cli/merger.rb +111 -0
- data/lib/archimate/diff/conflict.rb +31 -0
- data/lib/archimate/diff/conflicts.rb +89 -0
- data/lib/archimate/diff/conflicts/base_conflict.rb +53 -0
- data/lib/archimate/diff/conflicts/deleted_items_child_updated_conflict.rb +30 -0
- data/lib/archimate/diff/conflicts/deleted_items_referenced_conflict.rb +63 -0
- data/lib/archimate/diff/conflicts/path_conflict.rb +51 -0
- data/lib/archimate/diff/delete.rb +41 -0
- data/lib/archimate/diff/difference.rb +113 -0
- data/lib/archimate/diff/insert.rb +43 -0
- data/lib/archimate/diff/merge.rb +70 -0
- data/lib/archimate/diff/move.rb +51 -0
- data/lib/archimate/diff/version.rb +6 -0
- metadata +453 -0
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Conflicts
|
5
|
+
# BaseConflict
|
6
|
+
# @abstract
|
7
|
+
class BaseConflict
|
8
|
+
def initialize(base_local_diffs, base_remote_diffs)
|
9
|
+
@base_local_diffs = base_local_diffs
|
10
|
+
@base_remote_diffs = base_remote_diffs
|
11
|
+
@associative = false
|
12
|
+
@diff_iterations = nil
|
13
|
+
end
|
14
|
+
|
15
|
+
def filter1
|
16
|
+
->(_diff) { true }
|
17
|
+
end
|
18
|
+
|
19
|
+
def filter2
|
20
|
+
->(_diff) { true }
|
21
|
+
end
|
22
|
+
|
23
|
+
def conflicts
|
24
|
+
progressbar = ProgressIndicator.new(total: diff_iterations.size, title: "Analyzing Conflicts")
|
25
|
+
diff_iterations.each_with_object([]) do |(md1, md2), conflicts|
|
26
|
+
progressbar.increment
|
27
|
+
conflicts.concat(
|
28
|
+
md1.map { |diff1| [diff1, md2.select(&method(:diff_conflicts).curry[diff1])] }
|
29
|
+
.reject { |_diff1, diff2| diff2.empty? }
|
30
|
+
.map { |diff1, diff2_ary| Conflict.new(diff1, diff2_ary, describe) }
|
31
|
+
)
|
32
|
+
end
|
33
|
+
ensure
|
34
|
+
progressbar.finish
|
35
|
+
end
|
36
|
+
|
37
|
+
def diff_combinations
|
38
|
+
combos = [@base_local_diffs, @base_remote_diffs]
|
39
|
+
@associative ? [combos] : combos.permutation(2)
|
40
|
+
end
|
41
|
+
|
42
|
+
# By default our conflict tests are not associative to we need to run
|
43
|
+
# [local, remote] and [remote, local] through the tests.
|
44
|
+
def diff_iterations
|
45
|
+
@diff_iterations ||=
|
46
|
+
diff_combinations.map do |local_diffs, remote_diffs|
|
47
|
+
[local_diffs.select(&filter1), remote_diffs.select(&filter2)]
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Conflicts
|
5
|
+
class DeletedItemsChildUpdatedConflict < BaseConflict
|
6
|
+
def describe
|
7
|
+
"Checking for Deleted items in one change set have children that are inserted or changed in the other change set"
|
8
|
+
end
|
9
|
+
|
10
|
+
def filter1
|
11
|
+
->(diff) { diff.delete? }
|
12
|
+
end
|
13
|
+
|
14
|
+
def filter2
|
15
|
+
->(diff) { !diff.delete? }
|
16
|
+
end
|
17
|
+
|
18
|
+
# TODO: This is simple, but might be slow.
|
19
|
+
def diff_conflicts(diff1, diff2)
|
20
|
+
da1 = diff1.path.split("/")
|
21
|
+
da2 = diff2.path.split("/")
|
22
|
+
|
23
|
+
cmp_size = [da1, da2].map(&:size).min - 1
|
24
|
+
return false if da2.size == cmp_size + 1
|
25
|
+
da1[0..cmp_size] == da2[0..cmp_size]
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Conflicts
|
5
|
+
# DeletedItemsReferencedConflict
|
6
|
+
#
|
7
|
+
# This sort of conflict occurs when one change set has deleted an element
|
8
|
+
# that is referenced by id in the other change set.
|
9
|
+
#
|
10
|
+
# For example:
|
11
|
+
#
|
12
|
+
# In the local change set, an element with id "abc123" is deleted.
|
13
|
+
# In the remote change set, a child is inserted into a diagram with
|
14
|
+
# archimate_element = "abc123". These two changes are in conflict.
|
15
|
+
class DeletedItemsReferencedConflict < BaseConflict
|
16
|
+
using DataModel::DiffableArray
|
17
|
+
using DataModel::DiffablePrimitive
|
18
|
+
|
19
|
+
def describe
|
20
|
+
"Checking for Deleted items in one change set are referenced in the other change set"
|
21
|
+
end
|
22
|
+
|
23
|
+
# Filters a changeset to potentially conflicting diffs (making the set
|
24
|
+
# of combinations to check smaller)
|
25
|
+
#
|
26
|
+
# @return [lambda] a filter to limit diffs to Delete type
|
27
|
+
def filter1
|
28
|
+
->(diff) { diff.delete? && diff.target.value.is_a?(DataModel::Referenceable) }
|
29
|
+
end
|
30
|
+
|
31
|
+
# Filters a changeset to potentially conflicting diffs (making the set
|
32
|
+
# of combinations to check smaller)
|
33
|
+
#
|
34
|
+
# @return [lambda] a filter to limit diffs to other
|
35
|
+
# than Delete type
|
36
|
+
def filter2
|
37
|
+
->(diff) { !diff.delete? }
|
38
|
+
end
|
39
|
+
|
40
|
+
# Determine the set of conflicts between the given diffs
|
41
|
+
# def conflicts
|
42
|
+
# progressbar = ProgressIndicator.new(total: diff_iterations.size)
|
43
|
+
# diff_iterations.each_with_object([]) do |(md1, md2), a|
|
44
|
+
# progressbar.increment
|
45
|
+
# a.concat(
|
46
|
+
# md1.map { |diff1| [diff1, md2.select(&method(:diff_conflicts).curry[diff1])] }
|
47
|
+
# .reject { |_diff1, diff2| diff2.empty? }
|
48
|
+
# .map { |diff1, diff2_ary| Conflict.new(diff1, diff2_ary, describe) }
|
49
|
+
# )
|
50
|
+
# end
|
51
|
+
# ensure
|
52
|
+
# progressbar&.finish
|
53
|
+
# end
|
54
|
+
|
55
|
+
# TODO: This is simple, but might be slow. If it is, then override
|
56
|
+
# the conflicts method to prevent calculating referenced_identified_nodes methods
|
57
|
+
def diff_conflicts(diff1, diff2)
|
58
|
+
diff2.target.value.referenced_identified_nodes.include?(diff1.target.value.id)
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,51 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Conflicts
|
5
|
+
class PathConflict < BaseConflict
|
6
|
+
def initialize(base_local_diffs, base_remote_diffs)
|
7
|
+
super
|
8
|
+
@associative = true
|
9
|
+
end
|
10
|
+
|
11
|
+
def describe
|
12
|
+
"Checking for Differences in one change set that conflict with changes in other change set at the same path"
|
13
|
+
end
|
14
|
+
|
15
|
+
def diff_conflicts(diff1, diff2)
|
16
|
+
same_path_but_diff(diff1, diff2) && in_conflict?(diff1, diff2)
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def same_path_but_diff(a, b)
|
22
|
+
a.path == b.path && a != b
|
23
|
+
end
|
24
|
+
|
25
|
+
# I'm in conflict if:
|
26
|
+
# 1. ld and rd are both changes to the same path (but not the same change)
|
27
|
+
# 2. one is a change, the other a delete and changed_from is the same
|
28
|
+
# 3. both are inserts of the different identifyable nodes with the same id
|
29
|
+
#
|
30
|
+
# If I'm not an identifyable node and my parent is an array, then two inserts are ok
|
31
|
+
def in_conflict?(local_diff, remote_diff)
|
32
|
+
return true if
|
33
|
+
local_diff.target.parent.is_a?(Array) &&
|
34
|
+
local_diff.target.value.is_a?(DataModel::Referenceable) &&
|
35
|
+
local_diff.target.value.id == remote_diff.target.value.id &&
|
36
|
+
local_diff != remote_diff
|
37
|
+
|
38
|
+
case [local_diff, remote_diff].map { |d| d.class.name.split('::').last }.sort
|
39
|
+
when %w(Change Change)
|
40
|
+
local_diff.changed_from.value == remote_diff.changed_from.value &&
|
41
|
+
local_diff.target.value != remote_diff.target.value
|
42
|
+
when %w(Change Delete)
|
43
|
+
true
|
44
|
+
else
|
45
|
+
false
|
46
|
+
end
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
50
|
+
end
|
51
|
+
end
|
@@ -0,0 +1,41 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Delete < Difference
|
5
|
+
using DataModel::DiffablePrimitive
|
6
|
+
using DataModel::DiffableArray
|
7
|
+
|
8
|
+
# Create a new Delete difference
|
9
|
+
#
|
10
|
+
# @param target [ArchimateNodeReference] Element that was deleted
|
11
|
+
def initialize(target)
|
12
|
+
super
|
13
|
+
end
|
14
|
+
|
15
|
+
def to_s
|
16
|
+
# Note - the explicit to_s is required to access the DiffableArray
|
17
|
+
# implementation if the parent is an Array.
|
18
|
+
"#{diff_type} #{target} from #{target.parent.to_s}"
|
19
|
+
end
|
20
|
+
|
21
|
+
def apply(el)
|
22
|
+
target.delete(el)
|
23
|
+
el
|
24
|
+
end
|
25
|
+
|
26
|
+
def delete?
|
27
|
+
true
|
28
|
+
end
|
29
|
+
|
30
|
+
def kind
|
31
|
+
"Delete"
|
32
|
+
end
|
33
|
+
|
34
|
+
private
|
35
|
+
|
36
|
+
def diff_type
|
37
|
+
Color.color('DELETE:', :delete)
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
@@ -0,0 +1,113 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "forwardable"
|
4
|
+
|
5
|
+
module Archimate
|
6
|
+
module Diff
|
7
|
+
class Difference
|
8
|
+
using DataModel::DiffableArray
|
9
|
+
using DataModel::DiffablePrimitive
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
ARRAY_RE = Regexp.compile(/\[(\d+)\]/)
|
13
|
+
PATH_ROOT_SORT_ORDER = %w[elements relationships diagrams organizations].freeze
|
14
|
+
|
15
|
+
# delete: something responds to parent, child attribute (which is thing deleted - and could be sym for archimate nodes or index for array), value
|
16
|
+
# insert: something responds to parent, child attribute (or index), value, after value (to help with inserts)
|
17
|
+
# change: something responds to parent, child attribute (or index), value, changed from value
|
18
|
+
# move: something responds to parent, child index, value, after value) move after a particular value
|
19
|
+
attr_reader :target
|
20
|
+
attr_reader :changed_from
|
21
|
+
|
22
|
+
def_delegator :@target, :path
|
23
|
+
|
24
|
+
# Re-thinking.
|
25
|
+
#
|
26
|
+
# Requirements:
|
27
|
+
#
|
28
|
+
# 1. User friendly display of what is different in context
|
29
|
+
# 2. Able to apply the diff to another model (which was based on the "base" of the diff)
|
30
|
+
#
|
31
|
+
# Delete: example
|
32
|
+
# ArchimateNode child.bounds
|
33
|
+
# ArchimateNode, attribute model, "name"
|
34
|
+
# DiffableArray, ArchimateNode model.elements, element
|
35
|
+
# bendpoint attributes under connection
|
36
|
+
# documentation
|
37
|
+
# properties
|
38
|
+
# child/style/fill_color
|
39
|
+
# child/style/font/name
|
40
|
+
#
|
41
|
+
# @param target [Dry::Struct with id attribute] the element operated on (why is array treated as a special case?)
|
42
|
+
# @param changed_from [same class as target] (optional) for change this is the previous value
|
43
|
+
# def initialize(changed_from, target)
|
44
|
+
def initialize(target, changed_from = nil)
|
45
|
+
# raise TypeError, "Expected target to be an ArchimateNodeReference" unless target.is_a?(ArchimateNodeReference)
|
46
|
+
@target = target
|
47
|
+
@changed_from = changed_from
|
48
|
+
end
|
49
|
+
|
50
|
+
def ==(other)
|
51
|
+
other.is_a?(self.class) &&
|
52
|
+
@target == other.target &&
|
53
|
+
@changed_from == other.changed_from
|
54
|
+
end
|
55
|
+
|
56
|
+
# Difference sorting is based on the path.
|
57
|
+
# Top level components are sorted in this order: (elements, relationships, diagrams, organizations)
|
58
|
+
# Array entries are sorted by numeric order
|
59
|
+
# Others are sorted alphabetically
|
60
|
+
# TODO: this isn't complete
|
61
|
+
def <=>(other)
|
62
|
+
a = path_to_array
|
63
|
+
b = other.path_to_array
|
64
|
+
|
65
|
+
part_a = a.shift
|
66
|
+
part_b = b.shift
|
67
|
+
res = PATH_ROOT_SORT_ORDER.index(part_a) <=> PATH_ROOT_SORT_ORDER.index(part_b)
|
68
|
+
return res unless res.zero?
|
69
|
+
|
70
|
+
until a.empty? || b.empty?
|
71
|
+
part_a = a.shift
|
72
|
+
part_b = b.shift
|
73
|
+
|
74
|
+
return part_a <=> part_b unless (part_a <=> part_b).zero?
|
75
|
+
end
|
76
|
+
|
77
|
+
return -1 if a.empty?
|
78
|
+
return 1 if b.empty?
|
79
|
+
part_a <=> part_b
|
80
|
+
end
|
81
|
+
|
82
|
+
def delete?
|
83
|
+
false
|
84
|
+
end
|
85
|
+
|
86
|
+
def change?
|
87
|
+
false
|
88
|
+
end
|
89
|
+
|
90
|
+
def insert?
|
91
|
+
false
|
92
|
+
end
|
93
|
+
|
94
|
+
def move?
|
95
|
+
false
|
96
|
+
end
|
97
|
+
|
98
|
+
def path_to_array
|
99
|
+
path(force_array_index: :index).split("/").map do |p|
|
100
|
+
md = ARRAY_RE.match(p)
|
101
|
+
md ? md[1].to_i : p
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def summary_element
|
106
|
+
summary_elements = [DataModel::Element, DataModel::Organization, DataModel::Relationship, DataModel::Diagram, DataModel::Model]
|
107
|
+
se = target.value.primitive? ? target.parent : target.value
|
108
|
+
se = se.parent while summary_elements.none? { |c| se.is_a?(c) }
|
109
|
+
se
|
110
|
+
end
|
111
|
+
end
|
112
|
+
end
|
113
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
module Archimate
|
3
|
+
module Diff
|
4
|
+
class Insert < Difference
|
5
|
+
using DataModel::DiffableArray
|
6
|
+
|
7
|
+
# Create a new Insert difference
|
8
|
+
#
|
9
|
+
# @param inserted_element [Archimate::DataModel::ArchimateNode] Element
|
10
|
+
# that was inserted
|
11
|
+
# @param sub_path [str] Path under inserted_element for primitive values
|
12
|
+
def initialize(target)
|
13
|
+
super
|
14
|
+
end
|
15
|
+
|
16
|
+
def to_s
|
17
|
+
# Note - the explicit to_s is required to access the DiffableArray
|
18
|
+
# implementation if the parent is an Array.
|
19
|
+
"#{diff_type} #{target} into #{target.parent.to_s}"
|
20
|
+
end
|
21
|
+
|
22
|
+
def apply(to_model)
|
23
|
+
throw TypeError, "Expected a Archimate::DataModel::Model, was a #{to_model.class}" unless to_model.is_a?(DataModel::Model)
|
24
|
+
target.insert(to_model)
|
25
|
+
to_model
|
26
|
+
end
|
27
|
+
|
28
|
+
def insert?
|
29
|
+
true
|
30
|
+
end
|
31
|
+
|
32
|
+
def kind
|
33
|
+
"Insert"
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def diff_type
|
39
|
+
Color.color('INSERT:', :insert)
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parallel"
|
4
|
+
|
5
|
+
module Archimate
|
6
|
+
module Diff
|
7
|
+
class Merge
|
8
|
+
include Archimate::Logging
|
9
|
+
using DataModel::DiffableArray
|
10
|
+
|
11
|
+
def three_way(base, local, remote)
|
12
|
+
debug { "Computing base:local & base:remote diffs" }
|
13
|
+
base_local_diffs, base_remote_diffs = Parallel.map([[base, local], [base, remote]],
|
14
|
+
in_processes: 2) do |base_model, other_model|
|
15
|
+
base_model.diff(other_model)
|
16
|
+
end
|
17
|
+
|
18
|
+
debug "Finding Conflicts in #{base_local_diffs.size + base_remote_diffs.size} diffs"
|
19
|
+
conflicts = Conflicts.new(base_local_diffs, base_remote_diffs)
|
20
|
+
resolved_diffs = conflicts.resolve
|
21
|
+
|
22
|
+
[apply_diffs(resolved_diffs, base.clone), conflicts]
|
23
|
+
end
|
24
|
+
|
25
|
+
# Applies the set of diffs to the model returning a
|
26
|
+
# new model with the diffs applied.
|
27
|
+
def apply_diffs(diffs, model)
|
28
|
+
debug { "Applying #{diffs.size} diffs" }
|
29
|
+
progressbar = ProgressIndicator.new(total: diffs.size, title: "Applying diffs")
|
30
|
+
diffs
|
31
|
+
.inject(model) do |model_a, diff|
|
32
|
+
progressbar.increment
|
33
|
+
diff.apply(model_a)
|
34
|
+
end
|
35
|
+
.rebuild_index
|
36
|
+
.organize
|
37
|
+
ensure
|
38
|
+
progressbar&.finish
|
39
|
+
end
|
40
|
+
|
41
|
+
# # TODO: not currently used
|
42
|
+
# def find_merged_duplicates
|
43
|
+
# [@base_local_diffs, @base_remote_diffs].map do |diffs|
|
44
|
+
# deleted_element_diffs = diffs.select(&:delete?).select(&:element?)
|
45
|
+
# deleted_element_diffs.each_with_object({}) do |diff, a|
|
46
|
+
# element = diff.from_model.elements[diff.element_idx]
|
47
|
+
# found = diff.from_model.elements.select do |el|
|
48
|
+
# el != element && el.type == element.type && el.name == element.name
|
49
|
+
# end
|
50
|
+
# next if found.empty?
|
51
|
+
# a[diff] = found
|
52
|
+
# debug { "\nFound potential de-duplication:" }
|
53
|
+
# debug { "\t#{diff}" }
|
54
|
+
# debug { "Might be replaced with:\n\t#{found.map(&:to_s).join(" }\n\t")}\n\n"
|
55
|
+
# end
|
56
|
+
# end
|
57
|
+
# end
|
58
|
+
|
59
|
+
# # TODO: not currently used
|
60
|
+
# def filter_path_conflicts(diffs)
|
61
|
+
# diffs.sort { |a, b| a.path_to_array.size <=> b.path_to_array.size }.each_with_object([]) do |i, e|
|
62
|
+
# diffs.delete(i)
|
63
|
+
# path_conflicts = diffs.select { |d| d.path.start_with?(i.path) }
|
64
|
+
# path_conflicts.each { |d| diffs.delete(d) }
|
65
|
+
# e << i
|
66
|
+
# end
|
67
|
+
# end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|