upstream_merge.rb 3.03 KB
Newer Older
1 2 3 4
# frozen_string_literal: true

module ReleaseTools
  class UpstreamMerge
John Skarbek's avatar
John Skarbek committed
5
    attr_reader :origin, :upstream, :merge_branch
6

7
    CONFLICT_MARKER_REGEX = /\A(?<conflict_type>[ADU]{2}) /.freeze
8 9 10 11

    DownstreamAlreadyUpToDate = Class.new(StandardError)
    PushFailed = Class.new(StandardError)

John Skarbek's avatar
John Skarbek committed
12
    def initialize(origin:, upstream:, merge_branch:)
13 14
      @origin = origin
      @upstream = upstream
John Skarbek's avatar
John Skarbek committed
15
      @merge_branch = merge_branch
16 17 18 19 20 21 22 23 24 25 26 27 28 29
    end

    def execute!
      setup_merge_drivers
      prepare_upstream_merge
      conflicts = execute_upstream_merge
      after_upstream_merge

      conflicts
    end

    private

    def repository
John Skarbek's avatar
John Skarbek committed
30
      @repository ||= RemoteRepository.get({ origin: origin, upstream: upstream }, global_depth: 200)
31 32 33 34 35 36 37 38 39 40 41 42 43
    end

    def setup_merge_drivers
      repo = Rugged::Repository.new(repository.path)

      repo.config['merge.merge_db_schema.name'] = 'Merge db/schema.rb'
      repo.config['merge.merge_db_schema.driver'] = 'merge_db_schema %O %A %B'
      repo.config['merge.merge_db_schema.recursive'] = 'text'
    end

    def prepare_upstream_merge
      $stdout.puts "Prepare repository...".colorize(:green)
      # We fetch CE first to make sure our EE copy is more up-to-date!
John Skarbek's avatar
John Skarbek committed
44 45 46
      repository.fetch('master', remote: :upstream)
      repository.fetch('master', remote: :origin)
      repository.checkout_new_branch(merge_branch, base: 'origin/master')
47 48 49
    end

    def execute_upstream_merge
John Skarbek's avatar
John Skarbek committed
50
      result = repository.merge('upstream/master', merge_branch, no_ff: true)
51 52 53 54 55 56 57 58 59 60 61 62 63

      # Depending on Git version, it's "up-to-date" or "up to date"...
      raise DownstreamAlreadyUpToDate if result.output =~ /\AAlready up[\s\-]to[\s\-]date/

      conflicts = compute_conflicts
      conflicting_files = conflicts.map { |conflict_data| conflict_data[:path] }

      if conflicts.present?
        repository.commit(conflicting_files, no_edit: true)
        add_ci_skip_to_merge_commit
        add_latest_modifier_to_conflicts(conflicts)
      end

John Skarbek's avatar
John Skarbek committed
64
      raise PushFailed unless repository.push(origin, merge_branch)
65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101

      conflicts
    end

    def after_upstream_merge
      repository.cleanup
    end

    def compute_conflicts
      repository.status(short: true).lines.each_with_object([]) do |line, files|
        path = line.sub(CONFLICT_MARKER_REGEX, '').chomp
        # Store the file as key and conflict type as value, e.g.: { path: 'foo.rb', conflict_type: 'UU' }
        if line =~ CONFLICT_MARKER_REGEX
          files << { path: path, conflict_type: $LAST_MATCH_INFO[:conflict_type] }
        end
      end
    end

    def add_ci_skip_to_merge_commit
      repository.commit(nil, amend: true, message: "#{latest_commit_message}\n[ci skip]")
    end

    def latest_commit_message
      repository.log(latest: true, format: :message).chomp
    end

    def add_latest_modifier_to_conflicts(conflicts)
      conflicts.each do |conflict|
        conflict[:user] = latest_modifier(conflict[:path])
      end
    end

    def latest_modifier(file)
      repository.log(latest: true, no_merges: true, format: :author, paths: file).lines.first.chomp
    end
  end
end