Module: Canopus::Workspace::GitConflictResolution

Defined in:
lib/canopus/workspace/git_conflict_resolution.rb

Defined Under Namespace

Classes: GitConflictCapture, GitConflictContent, GitConflictPrepared, GitConflictSession, GitConflictView

Constant Summary collapse

MERGE_MAX_BYTES =
1 << 20
MERGE_MAX_LINES =
2_000
CONFLICT_MODES =
[0o100644, 0o100755, 0o120000].freeze
CONFLICT_CHOICES =
%i[ours theirs both manual].freeze

Instance Method Summary collapse

Instance Method Details

#close_git ⇒ Object



119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
# File 'lib/canopus/workspace/git_conflict_resolution.rb', line 119

def close_git
  @git_conflict_generation = (@git_conflict_generation || 0) + 1
  jobs = Array(@git_conflict_load_jobs).compact.reject { |thread| thread.equal?(Thread.current) }
  deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 0.1
  jobs.each do |thread|
    remaining = deadline - Process.clock_gettime(Process::CLOCK_MONOTONIC)
    break unless remaining.positive?
    thread.join(remaining)
  end
  jobs.each { |thread| thread.kill if thread.alive? }
  jobs.each(&:join)
  @git_conflict_resolve_job&.join unless @git_conflict_resolve_job.equal?(Thread.current)
  @git_conflict_load_jobs&.clear
  @git_conflict_session = nil
  super
end

#git_conflict_decorations(buffer, rows) ⇒ Object



92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
# File 'lib/canopus/workspace/git_conflict_resolution.rb', line 92

def git_conflict_decorations(buffer, rows)
  view = buffer.instance_variable_get(:@git_conflict_view)
  return [] unless view && view.session.equal?(@git_conflict_session)

  first = [rows.begin, 0].max
  last = [rows.exclude_end? ? rows.end : rows.end + 1, buffer.line_count].min
  items = (first...last).map do |row|
    Decoration::Item.new(:line, nil, row, nil, {color: "#f9758326"}.freeze,
      10, :merge_conflict, nil)
  end
  return items unless view.role == :ours && rows.cover?(0)

  choices = view.session.prepared.reason ? %i[ours theirs] : CONFLICT_CHOICES
  choices.each_with_index do |choice, index|
    label = {ours: "Use ours", theirs: "Use theirs", both: "Use both", manual: "Use manual edit"}.fetch(choice)
    click = lambda do |_current, _offset|
      resolve_git_conflict(choice, expected: view.session)
    rescue StandardError => error
      self.message = "Merge conflict: #{error.message}"
    end
    items << Decoration::Item.new(:block, nil, 0, label,
      {height: 20, position: :above, color: :accent, padding_left: index * 8}.freeze,
      20 + index, :merge_conflict, click)
  end
  items
end

#resolve_git_conflict(choice, content: nil, mode: nil, async: !!@window,, expected: nil) ⇒ Object

Raises:

  • (ArgumentError)


42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
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
# File 'lib/canopus/workspace/git_conflict_resolution.rb', line 42

def resolve_git_conflict(choice, content: nil, mode: nil, async: !!@window, expected: nil)
  raise ArgumentError, "invalid conflict choice" unless CONFLICT_CHOICES.include?(choice)
  raise ArgumentError, "content is only accepted for manual resolution" if content && choice != :manual
  if mode && !%i[both manual].include?(choice)
    raise ArgumentError, "mode is only accepted for both or manual resolution"
  end
  session = @git_conflict_session
  raise Error, "Open a merge conflict first" unless session
  raise Error, "Merge conflict view is stale" if expected && !expected.equal?(session)
  raise Error, "Conflict resolution already in progress" if @git_conflict_resolve_job
  if session.prepared.reason && !%i[ours theirs].include?(choice)
    raise Error, "#{session.prepared.reason}; choose ours or theirs"
  end
  unless mode.nil? || CONFLICT_MODES.include?(mode)
    raise ArgumentError, "invalid conflict resolution mode"
  end
  if session.mode && mode && session.mode != mode
    raise Error, "Conflict resolution mode changed"
  end
  session.mode ||= mode

  if session.result&.conflicts&.any?
    region = session.result.regions.first
    resolution = case choice
    when :ours then :ours
    when :theirs then :theirs
    when :both then :ours_then_theirs
    else
      content = session.buffers.fetch(:ours).text if content.nil?
      validate_manual_conflict_text(content)
    end
    resolved = Porrima::Merge.resolve(session.result, region.index, resolution)
    unless resolved.resolved?
      session.result = resolved
      session.choices << choice
      install_git_conflict_region(session)
      return session.editors.fetch(:ours)
    end
    content = Porrima::Merge.to_resolved_text(resolved)
  elsif session.result
    content = choice == :manual ? (content || session.buffers.fetch(:ours).text) :
      Porrima::Merge.to_resolved_text(session.result)
  elsif choice == :manual
    raise Error, "Manual resolution requires content" unless content.is_a?(String)
  end

  arguments = git_conflict_resolution_arguments(session, choice, content, session.mode)
  schedule_git_conflict_resolution(session, arguments, async: async)
end

#show_git_conflicts(path = nil, async: !!@window)) ⇒ Object Also known as: show_git_conflict

Raises:



17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# File 'lib/canopus/workspace/git_conflict_resolution.rb', line 17

def show_git_conflicts(path = nil, async: !!@window)
  state = git_state
  raise Error, "Not a Git repository" unless state
  raise Error, "Conflict resolution already in progress" if @git_conflict_resolve_job
  path = safe_git_diff_path(path) if path
  raise Error, "Finish the current manual conflict edit first" if git_conflict_editing?

  generation = @git_conflict_generation = (@git_conflict_generation || 0) + 1
  work = -> { prepare_git_conflict(capture_git_conflict(state, path)) }
  return install_git_conflict(work.call) unless @window && async

  @message = "Loading merge conflict…"
  worker = Thread.new do
    current = Thread.current
    prepared = work.call
    post { finish_git_conflict_load(current, generation, prepared, nil) } unless @closed
  rescue StandardError => error
    post { finish_git_conflict_load(current, generation, nil, error) } unless @closed
  end
  (@git_conflict_load_jobs ||= []) << worker
  worker
end