upstream_merge.rb 2.53 KB
Newer Older
1 2 3 4 5
require_relative 'remote_repository'

class UpstreamMerge
  attr_reader :origin, :upstream, :merge_branch

6
  CONFLICT_MARKER_REGEX = /\A(?<conflict_type>[ADU]{2}) /
7

8
  DownstreamAlreadyUpToDate = Class.new(StandardError)
Stan Hu's avatar
Stan Hu committed
9
  PushFailed = Class.new(StandardError)
10

11 12 13 14 15 16
  def initialize(origin:, upstream:, merge_branch:)
    @origin = origin
    @upstream = upstream
    @merge_branch = merge_branch
  end

17
  def execute!
18
    prepare_upstream_merge
19
    conflicts = execute_upstream_merge
20 21
    after_upstream_merge

22
    conflicts
23 24 25 26 27 28 29 30 31 32
  end

  private

  def repository
    @repository ||= RemoteRepository.get({ origin: origin, upstream: upstream }, global_depth: 200)
  end

  def prepare_upstream_merge
    $stdout.puts "Prepare repository...".colorize(:green)
33 34 35 36
    # We fetch CE first to make sure our EE copy is more up-to-date!
    repository.fetch('master', remote: :upstream)
    repository.fetch('master', remote: :origin)
    repository.checkout_new_branch(merge_branch, base: 'origin/master')
37 38 39
  end

  def execute_upstream_merge
40 41 42 43
    result = repository.merge('upstream/master', merge_branch, no_ff: true)

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

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

48
    if conflicts.present?
49 50
      repository.commit(conflicting_files, no_edit: true)
      add_ci_skip_to_merge_commit
51
      add_last_modifier_to_conflicts(conflicts)
52 53
    end

Stan Hu's avatar
Stan Hu committed
54
    raise PushFailed unless repository.push(origin, merge_branch)
55 56

    conflicts
57 58 59 60 61 62
  end

  def after_upstream_merge
    repository.cleanup
  end

63 64 65 66
  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' }
67
      if line =~ CONFLICT_MARKER_REGEX
68
        files << { path: path, conflict_type: $LAST_MATCH_INFO[:conflict_type] }
69
      end
70 71 72 73
    end
  end

  def add_ci_skip_to_merge_commit
74
    repository.commit(nil, amend: true, message: "#{latest_commit_message}\n[ci skip]")
75 76 77 78 79 80
  end

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

81 82 83 84 85 86
  def add_last_modifier_to_conflicts(conflicts)
    conflicts.each do |conflict|
      conflict[:user] = last_modifier(conflict[:path])
    end
  end

87 88 89 90
  def last_modifier(file)
    repository.log(paths: file, no_merges: true, format: :author).lines.first.chomp
  end
end