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,41 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "highline"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Archimate
         | 
| 6 | 
            +
              module Diff
         | 
| 7 | 
            +
                module Cli
         | 
| 8 | 
            +
                  class ConflictResolver
         | 
| 9 | 
            +
                    def initialize
         | 
| 10 | 
            +
                      @config = Config.instance
         | 
| 11 | 
            +
                      # TODO: pull the stdin/stdout from the app config
         | 
| 12 | 
            +
                      @hl = HighLine.new(STDIN, STDOUT)
         | 
| 13 | 
            +
                    end
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    # TODO: this implementation has much to be written
         | 
| 16 | 
            +
                    def resolve(conflict)
         | 
| 17 | 
            +
                      return [] unless @config.interactive
         | 
| 18 | 
            +
                      base_local_diffs = conflict.base_local_diffs
         | 
| 19 | 
            +
                      base_remote_diffs = conflict.base_remote_diffs
         | 
| 20 | 
            +
                      choice = @hl.choose do |menu|
         | 
| 21 | 
            +
                        menu.prompt = conflict
         | 
| 22 | 
            +
                        menu.choice(:local, text: base_local_diffs.map(&:to_s).join("\n\t\t"))
         | 
| 23 | 
            +
                        menu.choice(:remote, text: base_remote_diffs.map(&:to_s).join("\n\t\t"))
         | 
| 24 | 
            +
                        # menu.choice(:neither, help: "Don't choose either set of diffs")
         | 
| 25 | 
            +
                        # menu.choice(:edit, help: "Edit the diffs (coming soon)")
         | 
| 26 | 
            +
                        # menu.choice(:quit, help: "I'm in over my head. Just stop!")
         | 
| 27 | 
            +
                        menu.select_by = :index_or_name
         | 
| 28 | 
            +
                      end
         | 
| 29 | 
            +
                      case choice
         | 
| 30 | 
            +
                      when :local
         | 
| 31 | 
            +
                        base_local_diffs
         | 
| 32 | 
            +
                      when :remote
         | 
| 33 | 
            +
                        base_remote_diffs
         | 
| 34 | 
            +
                      else
         | 
| 35 | 
            +
                        error "Unexpected choice #{choice.inspect}."
         | 
| 36 | 
            +
                      end
         | 
| 37 | 
            +
                    end
         | 
| 38 | 
            +
                  end
         | 
| 39 | 
            +
                end
         | 
| 40 | 
            +
              end
         | 
| 41 | 
            +
            end
         | 
| @@ -0,0 +1,33 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            module Archimate
         | 
| 3 | 
            +
              module Diff
         | 
| 4 | 
            +
                module Cli
         | 
| 5 | 
            +
                  class Diff
         | 
| 6 | 
            +
                    attr_reader :local, :remote
         | 
| 7 | 
            +
             | 
| 8 | 
            +
                    def self.diff(local_file, remote_file)
         | 
| 9 | 
            +
                      local = Archimate.read(local_file)
         | 
| 10 | 
            +
                      remote = Archimate.read(remote_file)
         | 
| 11 | 
            +
             | 
| 12 | 
            +
                      my_diff = Diff.new(local, remote)
         | 
| 13 | 
            +
                      my_diff.diff
         | 
| 14 | 
            +
                    end
         | 
| 15 | 
            +
             | 
| 16 | 
            +
                    def initialize(local, remote)
         | 
| 17 | 
            +
                      @local = local
         | 
| 18 | 
            +
                      @remote = remote
         | 
| 19 | 
            +
                    end
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                    def diff
         | 
| 22 | 
            +
                      diffs = Archimate.diff(local, remote)
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                      diffs.each { |d| puts d }
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                      puts "\n\n#{diffs.size} Differences"
         | 
| 27 | 
            +
             | 
| 28 | 
            +
                      diffs
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
                  end
         | 
| 31 | 
            +
                end
         | 
| 32 | 
            +
              end
         | 
| 33 | 
            +
            end
         | 
| @@ -0,0 +1,103 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            require 'forwardable'
         | 
| 3 | 
            +
             | 
| 4 | 
            +
            module Archimate
         | 
| 5 | 
            +
              module Diff
         | 
| 6 | 
            +
                module Cli
         | 
| 7 | 
            +
                  class DiffSummary
         | 
| 8 | 
            +
                    using DataModel::DiffableArray
         | 
| 9 | 
            +
                    using DataModel::DiffablePrimitive
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    DIFF_KINDS = %w(Delete Change Insert).freeze
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    attr_reader :local, :remote
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                    def self.diff(local_file, remote_file, options = { verbose: true })
         | 
| 16 | 
            +
                      logger.info "Reading #{local_file}"
         | 
| 17 | 
            +
                      local = Archimate.read(local_file)
         | 
| 18 | 
            +
                      logger.info "Reading #{remote_file}"
         | 
| 19 | 
            +
                      remote = Archimate.read(remote_file)
         | 
| 20 | 
            +
             | 
| 21 | 
            +
                      my_diff = DiffSummary.new(local, remote)
         | 
| 22 | 
            +
                      my_diff.diff
         | 
| 23 | 
            +
                    end
         | 
| 24 | 
            +
             | 
| 25 | 
            +
                    def initialize(local, remote)
         | 
| 26 | 
            +
                      @local = local
         | 
| 27 | 
            +
                      @remote = remote
         | 
| 28 | 
            +
                      @summary = Hash.new { |hash, key| hash[key] = Hash.new { |k_hash, k_key| k_hash[k_key] = 0 } }
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def diff
         | 
| 32 | 
            +
                      logger.info "Calculating differences"
         | 
| 33 | 
            +
                      diffs = Archimate.diff(local, remote)
         | 
| 34 | 
            +
             | 
| 35 | 
            +
                      puts Color.color("Summary of differences", :headline)
         | 
| 36 | 
            +
                      puts "\n"
         | 
| 37 | 
            +
             | 
| 38 | 
            +
                      summary_element_diffs = diffs.group_by { |diff| diff.summary_element.class.to_s.split("::").last }
         | 
| 39 | 
            +
                      summarize_elements summary_element_diffs["Element"]
         | 
| 40 | 
            +
                      summarize "Organization", summary_element_diffs["Organization"]
         | 
| 41 | 
            +
                      summarize "Relationship", summary_element_diffs["Relationship"]
         | 
| 42 | 
            +
                      summarize_diagrams summary_element_diffs["Diagram"]
         | 
| 43 | 
            +
             | 
| 44 | 
            +
                      puts "Total Diffs: #{diffs.size}"
         | 
| 45 | 
            +
                    end
         | 
| 46 | 
            +
             | 
| 47 | 
            +
                    def summarize(title, diffs)
         | 
| 48 | 
            +
                      return if diffs.nil? || diffs.empty?
         | 
| 49 | 
            +
                      by_kind = diffs_by_kind(diffs)
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                      puts color(title)
         | 
| 52 | 
            +
                      DIFF_KINDS.each do |kind|
         | 
| 53 | 
            +
                        puts format("  #{color(kind)}: #{by_kind[kind]&.size}") if by_kind.key?(kind)
         | 
| 54 | 
            +
                      end
         | 
| 55 | 
            +
                    end
         | 
| 56 | 
            +
             | 
| 57 | 
            +
                    def diffs_by_kind(diffs)
         | 
| 58 | 
            +
                      diffs
         | 
| 59 | 
            +
                        .group_by(&:summary_element)
         | 
| 60 | 
            +
                        .each_with_object(Hash.new { |hash, key| hash[key] = [] }) do |(summary_element, element_diffs), a|
         | 
| 61 | 
            +
                        top_level_diff = element_diffs.find { |diff| summary_element == diff.target.value }
         | 
| 62 | 
            +
                        if top_level_diff
         | 
| 63 | 
            +
                          a[top_level_diff.kind] << summary_element
         | 
| 64 | 
            +
                        else
         | 
| 65 | 
            +
                          a["Change"] << summary_element
         | 
| 66 | 
            +
                        end
         | 
| 67 | 
            +
                      end
         | 
| 68 | 
            +
                    end
         | 
| 69 | 
            +
             | 
| 70 | 
            +
                    def summarize_elements(diffs)
         | 
| 71 | 
            +
                      return if diffs.nil? || diffs.empty?
         | 
| 72 | 
            +
                      puts Color.color("Elements", :headline)
         | 
| 73 | 
            +
                      by_layer = diffs.group_by { |diff| diff.summary_element.layer }
         | 
| 74 | 
            +
                      summarize "Business", by_layer["Business"]
         | 
| 75 | 
            +
                      summarize "Application", by_layer["Application"]
         | 
| 76 | 
            +
                      summarize "Technology", by_layer["Technology"]
         | 
| 77 | 
            +
                      summarize "Motivation", by_layer["Motivation"]
         | 
| 78 | 
            +
                      summarize "Implementation and Migration", by_layer["Implementation and Migration"]
         | 
| 79 | 
            +
                      summarize "Connectors", by_layer["Connectors"]
         | 
| 80 | 
            +
                    end
         | 
| 81 | 
            +
             | 
| 82 | 
            +
                    def summarize_diagrams(diffs)
         | 
| 83 | 
            +
                      return if diffs.nil? || diffs.empty?
         | 
| 84 | 
            +
                      puts Color.color("Diagrams", :headline)
         | 
| 85 | 
            +
             | 
| 86 | 
            +
                      by_kind = diffs_by_kind(diffs)
         | 
| 87 | 
            +
                      %w(Delete Change Insert).each do |kind|
         | 
| 88 | 
            +
                        next unless by_kind.key?(kind)
         | 
| 89 | 
            +
                        diagram_names = by_kind[kind].uniq.map(&:name)
         | 
| 90 | 
            +
                        puts "  #{color(kind)}"
         | 
| 91 | 
            +
                        # TODO: make this magic number an option
         | 
| 92 | 
            +
                        diagram_names[0..14].each { |diagram_name| puts "    #{diagram_name}" }
         | 
| 93 | 
            +
                        puts "    ... and #{diagram_names.size - 15} more" if diagram_names.size > 15
         | 
| 94 | 
            +
                      end
         | 
| 95 | 
            +
                    end
         | 
| 96 | 
            +
             | 
| 97 | 
            +
                    def color(kind)
         | 
| 98 | 
            +
                      Color.color(kind, kind)
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
                  end
         | 
| 101 | 
            +
                end
         | 
| 102 | 
            +
              end
         | 
| 103 | 
            +
            end
         | 
| @@ -0,0 +1,51 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require "parallel"
         | 
| 4 | 
            +
             | 
| 5 | 
            +
            module Archimate
         | 
| 6 | 
            +
              module Diff
         | 
| 7 | 
            +
                module Cli
         | 
| 8 | 
            +
                  class Merge
         | 
| 9 | 
            +
                    include Logging
         | 
| 10 | 
            +
             | 
| 11 | 
            +
                    attr_reader :base, :local, :remote, :merged_file
         | 
| 12 | 
            +
             | 
| 13 | 
            +
                    def self.merge(base_file, remote_file, local_file, merged_file)
         | 
| 14 | 
            +
                      Logging.debug { "Reading base file: #{base_file}, local file: #{local_file}, remote file: #{remote_file}" }
         | 
| 15 | 
            +
                      base, local, remote = Parallel.map([base_file, local_file, remote_file], in_processes: 3) do |file|
         | 
| 16 | 
            +
                        Archimate.read(file)
         | 
| 17 | 
            +
                      end
         | 
| 18 | 
            +
                      Logging.debug { "Merged file is #{merged_file}" }
         | 
| 19 | 
            +
             | 
| 20 | 
            +
                      Merge.new(base, local, remote, merged_file).run_merge
         | 
| 21 | 
            +
                    end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                    def initialize(base, local, remote, merged_file)
         | 
| 24 | 
            +
                      @base = base
         | 
| 25 | 
            +
                      @local = local
         | 
| 26 | 
            +
                      @remote = remote
         | 
| 27 | 
            +
                      @merged_file = merged_file
         | 
| 28 | 
            +
                      @merge = Archimate::Diff::Merge.new
         | 
| 29 | 
            +
                    end
         | 
| 30 | 
            +
             | 
| 31 | 
            +
                    def run_merge
         | 
| 32 | 
            +
                      debug { "Starting merging" }
         | 
| 33 | 
            +
                      merged, conflicts = @merge.three_way(base, local, remote)
         | 
| 34 | 
            +
                      # TODO: there should be no conflicts here
         | 
| 35 | 
            +
                      debug do
         | 
| 36 | 
            +
                        <<~MSG
         | 
| 37 | 
            +
                          Done merging
         | 
| 38 | 
            +
                          #{conflicts}
         | 
| 39 | 
            +
                        MSG
         | 
| 40 | 
            +
                      end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                      File.open(merged_file, "w") do |file|
         | 
| 43 | 
            +
                        # TODO: this should be controlled by the options and the defaulted to the read format
         | 
| 44 | 
            +
                        debug { "Serializing" }
         | 
| 45 | 
            +
                        Archimate::FileFormats::ArchiFileWriter.write(merged, file)
         | 
| 46 | 
            +
                      end
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
                  end
         | 
| 49 | 
            +
                end
         | 
| 50 | 
            +
              end
         | 
| 51 | 
            +
            end
         | 
| @@ -0,0 +1,111 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            module Archimate
         | 
| 4 | 
            +
              module Diff
         | 
| 5 | 
            +
                module Cli
         | 
| 6 | 
            +
                  # Merger is a class that is a decorator on Archimate::DataModel::Model
         | 
| 7 | 
            +
                  # to provide the capability to merge another model into itself
         | 
| 8 | 
            +
                  #
         | 
| 9 | 
            +
                  # TODO: provide for a conflict resolver instance
         | 
| 10 | 
            +
                  # TODO: provide an option to determine if potential matches are merged
         | 
| 11 | 
            +
                  #       or if the conflict resolver should be asked.
         | 
| 12 | 
            +
                  class Merger # < SimpleDelegator
         | 
| 13 | 
            +
                    # def initialize(primary_model, conflict_resolver)
         | 
| 14 | 
            +
                    #   super(primary_model)
         | 
| 15 | 
            +
                    #   @resolver = conflict_resolver
         | 
| 16 | 
            +
                    # end
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                    # What merge does:
         | 
| 19 | 
            +
                    # For all entities: (other than Model...):
         | 
| 20 | 
            +
                    #   - PropertyDefinition
         | 
| 21 | 
            +
                    #   - View
         | 
| 22 | 
            +
                    #   - Viewpoint
         | 
| 23 | 
            +
                    # Entity:
         | 
| 24 | 
            +
                    #   look for a matching entity: with result
         | 
| 25 | 
            +
                    #     1. Found a matching entity: goto entity merge
         | 
| 26 | 
            +
                    #     2. Found no matching entity, but id conflicts: gen new id, goto add entity
         | 
| 27 | 
            +
                    #     3. Found no matching entity: goto add entity
         | 
| 28 | 
            +
                    #   entity merge:
         | 
| 29 | 
            +
                    #     1. merge (with func from deduper)
         | 
| 30 | 
            +
                    #   add entity:
         | 
| 31 | 
            +
                    #     1. add entity to model
         | 
| 32 | 
            +
                    #   add remapping entry to map from entities other model id to id in this model
         | 
| 33 | 
            +
                    # Relationship:
         | 
| 34 | 
            +
                    # def merge(other_model)
         | 
| 35 | 
            +
                    #   other_model.entities.each do |entity|
         | 
| 36 | 
            +
                    #     # TODO: matching entity should use the same criteria that DuplicateEntities uses.
         | 
| 37 | 
            +
                    #     my_entity = find_matching_entity(entity)
         | 
| 38 | 
            +
                    #     if my_entity
         | 
| 39 | 
            +
                    #   end
         | 
| 40 | 
            +
                    # end
         | 
| 41 | 
            +
             | 
| 42 | 
            +
                    # TODO: handle inner text of elements
         | 
| 43 | 
            +
                    # TODO: handle merging by element type
         | 
| 44 | 
            +
             | 
| 45 | 
            +
                    def hash_to_attr(h)
         | 
| 46 | 
            +
                      h.map { |k, v| "#{k}=\"#{v}\"" }.join(" ")
         | 
| 47 | 
            +
                    end
         | 
| 48 | 
            +
             | 
| 49 | 
            +
                    def e_to_s(e)
         | 
| 50 | 
            +
                      "#{e.name} #{hash_to_attr(e.attributes)}"
         | 
| 51 | 
            +
                    end
         | 
| 52 | 
            +
             | 
| 53 | 
            +
                    # Merge node1, node2
         | 
| 54 | 
            +
                    # For node
         | 
| 55 | 
            +
                    #   For each child
         | 
| 56 | 
            +
                    #     If has a matching child
         | 
| 57 | 
            +
                    def merge(doc1, doc2)
         | 
| 58 | 
            +
                      doc2.children.each do |e|
         | 
| 59 | 
            +
                        next if e.name == "text" && e.text.strip.empty?
         | 
| 60 | 
            +
                        # p = e.path
         | 
| 61 | 
            +
                        # if p =~ /\[\d+\]$/
         | 
| 62 | 
            +
                        #   p = p.gsub(/\[\d+\]$/, "[@name=\"#{e.attr("name")}\"]")
         | 
| 63 | 
            +
                        # end
         | 
| 64 | 
            +
                        # puts "Looking for #{p}"``
         | 
| 65 | 
            +
                        # matches = doc1.xpath(p)
         | 
| 66 | 
            +
                        css = ">#{e.name}"
         | 
| 67 | 
            +
                        # puts css
         | 
| 68 | 
            +
                        css += "[name=\"#{e.attr('name')}\"]" if e.attributes.include?("name")
         | 
| 69 | 
            +
                        css += "[xsi|type=\"#{e.attr('xsi:type')}\"]" if e.attributes.include?("xsi:type")
         | 
| 70 | 
            +
                        matches = doc1.css(css)
         | 
| 71 | 
            +
                        if !matches.empty? # We have a potential match
         | 
| 72 | 
            +
                          # puts "Match?"
         | 
| 73 | 
            +
                          # puts "  Doc2: #{e_to_s(e)}"
         | 
| 74 | 
            +
                          # matches.each do |e1|
         | 
| 75 | 
            +
                          #   puts "  Doc1: #{e_to_s(e1)}"
         | 
| 76 | 
            +
                          # end
         | 
| 77 | 
            +
                          merge(matches[0], e) unless matches.size > 1
         | 
| 78 | 
            +
                        else # No match insert the node into the tree TODO: handle id conflicts
         | 
| 79 | 
            +
                          doc1.add_child(e)
         | 
| 80 | 
            +
                        end
         | 
| 81 | 
            +
                      end
         | 
| 82 | 
            +
                      doc1
         | 
| 83 | 
            +
                    end
         | 
| 84 | 
            +
             | 
| 85 | 
            +
                    def id_hash_for(doc)
         | 
| 86 | 
            +
                      doc.css("[id]").each_with_object({}) do |obj, memo|
         | 
| 87 | 
            +
                        memo[obj["id"]] = obj
         | 
| 88 | 
            +
                        memo
         | 
| 89 | 
            +
                      end
         | 
| 90 | 
            +
                    end
         | 
| 91 | 
            +
             | 
| 92 | 
            +
                    def conflicting_ids(doc1, doc2)
         | 
| 93 | 
            +
                      doc_id_hash1 = id_hash_for(doc1)
         | 
| 94 | 
            +
                      doc_id_hash2 = id_hash_for(doc2)
         | 
| 95 | 
            +
                      cids = Set.new(doc_id_hash1.keys) & doc_id_hash2.keys
         | 
| 96 | 
            +
                      # puts "ID Conflicts:"
         | 
| 97 | 
            +
                      # puts cids.to_a.join(",")
         | 
| 98 | 
            +
                      cids
         | 
| 99 | 
            +
                    end
         | 
| 100 | 
            +
             | 
| 101 | 
            +
                    def merge_files(file1, file2)
         | 
| 102 | 
            +
                      doc1 = Nokogiri::XML(File.open(file1))
         | 
| 103 | 
            +
                      doc2 = Nokogiri::XML(File.open(file2))
         | 
| 104 | 
            +
             | 
| 105 | 
            +
                      # cids = conflicting_ids(doc1, doc2)
         | 
| 106 | 
            +
                      merge(doc1.root, doc2.root).document
         | 
| 107 | 
            +
                    end
         | 
| 108 | 
            +
                  end
         | 
| 109 | 
            +
                end
         | 
| 110 | 
            +
              end
         | 
| 111 | 
            +
            end
         | 
| @@ -0,0 +1,31 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
            module Archimate
         | 
| 3 | 
            +
              module Diff
         | 
| 4 | 
            +
                class Conflict
         | 
| 5 | 
            +
                  attr_reader :base_local_diffs
         | 
| 6 | 
            +
                  attr_reader :base_remote_diffs
         | 
| 7 | 
            +
                  attr_reader :reason
         | 
| 8 | 
            +
                  attr_reader :diffs
         | 
| 9 | 
            +
             | 
| 10 | 
            +
                  def initialize(base_local_diffs, base_remote_diffs, reason)
         | 
| 11 | 
            +
                    @base_local_diffs = Array(base_local_diffs)
         | 
| 12 | 
            +
                    @base_remote_diffs = Array(base_remote_diffs)
         | 
| 13 | 
            +
                    @diffs = @base_local_diffs + @base_remote_diffs
         | 
| 14 | 
            +
                    @reason = reason
         | 
| 15 | 
            +
                  end
         | 
| 16 | 
            +
             | 
| 17 | 
            +
                  def to_s
         | 
| 18 | 
            +
                    "#{Color.color('CONFLICT:', [:black, :on_red])} #{reason}\n" \
         | 
| 19 | 
            +
                      "\tBase->Local Diff(s):\n\t\t#{base_local_diffs.map(&:to_s).join("\n\t\t")}" \
         | 
| 20 | 
            +
                      "\n\tBase->Remote Diffs(s):\n\t\t#{base_remote_diffs.map(&:to_s).join("\n\t\t")}"
         | 
| 21 | 
            +
                  end
         | 
| 22 | 
            +
             | 
| 23 | 
            +
                  def ==(other)
         | 
| 24 | 
            +
                    other.is_a?(self.class) &&
         | 
| 25 | 
            +
                      base_local_diffs == other.base_local_diffs &&
         | 
| 26 | 
            +
                      base_remote_diffs == other.base_remote_diffs &&
         | 
| 27 | 
            +
                      reason == other.reason
         | 
| 28 | 
            +
                  end
         | 
| 29 | 
            +
                end
         | 
| 30 | 
            +
              end
         | 
| 31 | 
            +
            end
         | 
| @@ -0,0 +1,89 @@ | |
| 1 | 
            +
            # frozen_string_literal: true
         | 
| 2 | 
            +
             | 
| 3 | 
            +
            require 'forwardable'
         | 
| 4 | 
            +
            require 'parallel'
         | 
| 5 | 
            +
            require 'archimate/diff/conflicts/base_conflict'
         | 
| 6 | 
            +
            require 'archimate/diff/conflicts/deleted_items_child_updated_conflict'
         | 
| 7 | 
            +
            require 'archimate/diff/conflicts/deleted_items_referenced_conflict'
         | 
| 8 | 
            +
            require 'archimate/diff/conflicts/path_conflict'
         | 
| 9 | 
            +
             | 
| 10 | 
            +
            module Archimate
         | 
| 11 | 
            +
              module Diff
         | 
| 12 | 
            +
                class Conflicts
         | 
| 13 | 
            +
                  extend Forwardable
         | 
| 14 | 
            +
             | 
| 15 | 
            +
                  attr_reader :base_local_diffs
         | 
| 16 | 
            +
                  attr_reader :base_remote_diffs
         | 
| 17 | 
            +
             | 
| 18 | 
            +
                  def_delegator :@conflicts, :empty?
         | 
| 19 | 
            +
                  def_delegator :@conflicts, :size
         | 
| 20 | 
            +
                  def_delegator :@conflicts, :first
         | 
| 21 | 
            +
                  def_delegator :@conflicts, :map
         | 
| 22 | 
            +
                  def_delegator :@conflicts, :each
         | 
| 23 | 
            +
             | 
| 24 | 
            +
                  include Archimate::Logging
         | 
| 25 | 
            +
             | 
| 26 | 
            +
                  def initialize(base_local_diffs, base_remote_diffs)
         | 
| 27 | 
            +
                    @base_local_diffs = base_local_diffs
         | 
| 28 | 
            +
                    @base_remote_diffs = base_remote_diffs
         | 
| 29 | 
            +
                    @conflict_finders = [PathConflict, DeletedItemsChildUpdatedConflict, DeletedItemsReferencedConflict]
         | 
| 30 | 
            +
                    @conflicts = nil
         | 
| 31 | 
            +
                    @conflicting_diffs = nil
         | 
| 32 | 
            +
                    @unconflicted_diffs = nil
         | 
| 33 | 
            +
                    # TODO: consider making this an argument
         | 
| 34 | 
            +
                    @conflict_resolver = Cli::ConflictResolver.new
         | 
| 35 | 
            +
                  end
         | 
| 36 | 
            +
             | 
| 37 | 
            +
                  # TODO: refactor this method elsewhere
         | 
| 38 | 
            +
                  # resolve iterates through the set of conflicting diffs asking the user
         | 
| 39 | 
            +
                  # (if running interactively) and return the set of diffs that can be applied.
         | 
| 40 | 
            +
                  #
         | 
| 41 | 
            +
                  # To keep diffs reasonably human readable in logs, the local diffs should
         | 
| 42 | 
            +
                  # be applied first followed by the remote diffs.
         | 
| 43 | 
            +
                  def resolve
         | 
| 44 | 
            +
                    debug do
         | 
| 45 | 
            +
                      <<~MSG
         | 
| 46 | 
            +
                        Filtering out #{conflicts.size} conflicts from #{base_local_diffs.size + base_remote_diffs.size} diffs
         | 
| 47 | 
            +
                        Remaining diffs #{unconflicted_diffs.size}
         | 
| 48 | 
            +
                      MSG
         | 
| 49 | 
            +
                    end
         | 
| 50 | 
            +
             | 
| 51 | 
            +
                    conflicts.each_with_object(unconflicted_diffs) do |conflict, diffs|
         | 
| 52 | 
            +
                      # TODO: this will result in diffs being out of order from their
         | 
| 53 | 
            +
                      # original order. diffs should be flagged as conflicted and
         | 
| 54 | 
            +
                      # this method should instead remove the conflicted flag.
         | 
| 55 | 
            +
                      diffs.concat(@conflict_resolver.resolve(conflict))
         | 
| 56 | 
            +
                      # TODO: if the conflict is resolved, it should be removed from the
         | 
| 57 | 
            +
                      # @conflicts array.
         | 
| 58 | 
            +
                    end
         | 
| 59 | 
            +
                  end
         | 
| 60 | 
            +
             | 
| 61 | 
            +
                  def conflicts
         | 
| 62 | 
            +
                    @conflicts ||= find_conflicts
         | 
| 63 | 
            +
                  end
         | 
| 64 | 
            +
             | 
| 65 | 
            +
                  def conflicting_diffs
         | 
| 66 | 
            +
                    @conflicting_diffs ||= conflicts.map(&:diffs).flatten
         | 
| 67 | 
            +
                  end
         | 
| 68 | 
            +
             | 
| 69 | 
            +
                  def unconflicted_diffs
         | 
| 70 | 
            +
                    @unconflicted_diffs ||=
         | 
| 71 | 
            +
                      (base_local_diffs + base_remote_diffs) - conflicting_diffs
         | 
| 72 | 
            +
                  end
         | 
| 73 | 
            +
             | 
| 74 | 
            +
                  def to_s
         | 
| 75 | 
            +
                    "Conflicts:\n\n#{conflicts.map(&:to_s).join("\n\n")}\n"
         | 
| 76 | 
            +
                  end
         | 
| 77 | 
            +
             | 
| 78 | 
            +
                  private
         | 
| 79 | 
            +
             | 
| 80 | 
            +
                  def find_conflicts
         | 
| 81 | 
            +
                    # TODO: Running this in parallel breaks currently.
         | 
| 82 | 
            +
                    # @conflicts = Parallel.map(@conflict_finders, in_processes: 3) { |cf_class|
         | 
| 83 | 
            +
                    @conflicts = @conflict_finders.map { |cf_class|
         | 
| 84 | 
            +
                      cf_class.new(base_local_diffs, base_remote_diffs).conflicts
         | 
| 85 | 
            +
                    }.flatten
         | 
| 86 | 
            +
                  end
         | 
| 87 | 
            +
                end
         | 
| 88 | 
            +
              end
         | 
| 89 | 
            +
            end
         |