Module: Canopus::Workspace::GitDiffView

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

Defined Under Namespace

Classes: GitDiffSource, GitDiffView, GitHistoryEntry

Constant Summary collapse

MAX_DIFF_BYTES =
10 << 20
MAX_HISTORY =
200
MAX_HISTORY_SCAN =
20_000
MAX_PATH_BYTES =
4_096
MAX_REVISION_BYTES =
1_024
SIDE_COLUMN_WIDTH =
160
DIFF_BUDGET =
Porrima::Budget.new(max_bytes: 1 << 20, max_lines: 2_000)
DELETED_STYLE =
{color: "#f9758344"}.freeze
INSERTED_STYLE =
{color: "#80b98744"}.freeze

Instance Method Summary collapse

Instance Method Details

#close_git ⇒ Object



163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
# File 'lib/canopus/workspace/git_diff_view.rb', line 163

def close_git
  @git_view_generation = (@git_view_generation || 0) + 1
  @git_history_generation = (@git_history_generation || 0) + 1
  jobs = [*@git_view_jobs, *@git_history_jobs].compact.uniq.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_view_jobs&.clear
  @git_history_jobs&.clear
  super
end

#compare_git_revisions(before, after, path: nil, mode: :inline, async: !!@window)) ⇒ Object

Raises:



58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
# File 'lib/canopus/workspace/git_diff_view.rb', line 58

def compare_git_revisions(before, after, path: nil, mode: :inline, async: !!@window)
  state = git_state
  raise Error, "Not a Git repository" unless state
  path = safe_git_diff_path(git_diff_target_path(path))
  before = validate_git_revision(before, allow_nil: true)
  after = validate_git_revision(after)
  mode = normalize_git_diff_mode(mode)
  schedule_git_diff(mode: mode, async: async) do
    captured = state.synchronize do |repository|
      left_commit = resolve_git_revision(repository, before)
      right_commit = resolve_git_revision(repository, after)
      left, left_text = revision_file_content(repository, left_commit, path)
      right, right_text = revision_file_content(repository, right_commit, path)
       = (left, right) if left_text == right_text &&
        (left.exists != right.exists || left.mode != right.mode)
      unavailable = git_diff_unavailable(path, left_text, right_text, byte_values: [left.raw, right.raw])
      [left_text, right_text, , unavailable].freeze
    end
    build_git_diff_source(path, captured[0], captured[1], snapshot: nil, metadata: captured[2],
      unavailable: captured[3], before_label: revision_label(path, before), after_label: revision_label(path, after))
  end
end

#git_file_history(path = nil, revision: "HEAD", limit: 100) ⇒ Object

Raises:



95
96
97
98
99
100
101
102
103
104
105
# File 'lib/canopus/workspace/git_diff_view.rb', line 95

def git_file_history(path = nil, revision: "HEAD", limit: 100)
  state = git_state
  raise Error, "Not a Git repository" unless state
  path = safe_git_diff_path(git_diff_target_path(path))
  revision = validate_git_revision(revision)
  unless limit.is_a?(Integer) && limit.between?(1, MAX_HISTORY)
    raise ArgumentError, "history limit must be between 1 and #{MAX_HISTORY}"
  end

  state.synchronize { |repository| collect_git_file_history(repository, path, revision, limit) }
end

#scm_diff_decorations(buffer, rows) ⇒ Object



133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
# File 'lib/canopus/workspace/git_diff_view.rb', line 133

def scm_diff_decorations(buffer, rows)
  view = buffer.instance_variable_get(:@git_diff_view)
  return super unless view

  items = view.highlights.select do |item|
    row = buffer.rope.point_at(item.range.begin).row
    rows.cover?(row)
  end
  snapshot = view.source.snapshot
  return items unless snapshot&.diff && !snapshot.conflicted

  action = snapshot.kind == :staged ? :unstage : :stage
  verb = action == :stage ? "Stage" : "Unstage"
  view.line_rows.each do |display_row, index|
    next unless rows.cover?(display_row)
    click = ->(current, row) { change_scm_line(action, index, current.buffer, row) }
    items << Decoration::Item.new(:gutter, nil, display_row, "#{verb} line",
      {color: :accent, gutter_offset: 2, gutter_width: 3, hit_width: 10}.freeze,
      1, :scm_diff, click)
  end
  view.hunk_rows.each do |display_row, index|
    next unless rows.cover?(display_row) && !view.line_rows.key?(display_row)
    click = ->(current, row) { change_scm_hunk(action, index, current.buffer, row) }
    items << Decoration::Item.new(:gutter, nil, display_row, "#{verb} hunk",
      {color: :muted, gutter_offset: 2, gutter_width: 3, hit_width: 10}.freeze,
      2, :scm_diff, click)
  end
  items
end

#show_git_diff(mode: :inline, async: !!@window)) ⇒ Object

Raises:



42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
# File 'lib/canopus/workspace/git_diff_view.rb', line 42

def show_git_diff(mode: :inline, async: !!@window)
  current = editor
  path = safe_git_diff_path(git_relative_path(current.buffer))
  after_size = current.buffer.rope.bytesize
  after = after_size <= MAX_DIFF_BYTES ? current.buffer.text.dup.freeze : nil
  state = git_state
  raise Error, "Not a Git repository" unless state
  mode = normalize_git_diff_mode(mode)
  schedule_git_diff(mode: mode, async: async) do
    raw = state.synchronize { |repository| repository.blob(path) }
    before = decode_git_diff_content(raw)
    unavailable = git_diff_unavailable(path, before, after, byte_values: [raw, after_size])
    build_git_diff_source(path, before, after, unavailable: unavailable)
  end
end

#show_git_file_history(path = nil, revision: "HEAD", limit: 100, async: !!@window)) ⇒ Object



107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
# File 'lib/canopus/workspace/git_diff_view.rb', line 107

def show_git_file_history(path = nil, revision: "HEAD", limit: 100, async: !!@window)
  path = safe_git_diff_path(git_diff_target_path(path))
  revision = validate_git_revision(revision)
  if @window && async
    generation = @git_history_generation = (@git_history_generation || 0) + 1
    palette_generation = @palette_generation.to_i
    @message = "Loading Git history…"
    worker = Thread.new do
      current_worker = Thread.current
      entries = git_file_history(path, revision: revision, limit: limit)
      post { finish_git_history(current_worker, generation, palette_generation, path, entries, nil) } unless @closed
    rescue StandardError => error
      post { finish_git_history(current_worker, generation, palette_generation, path, nil, error) } unless @closed
    end
    (@git_history_jobs ||= []) << worker
    return worker
  end

  install_git_history(path, git_file_history(path, revision: revision, limit: limit))
end

#show_git_revision_compare(path = nil) ⇒ Object



128
129
130
131
# File 'lib/canopus/workspace/git_diff_view.rb', line 128

def show_git_revision_compare(path = nil)
  path = safe_git_diff_path(git_diff_target_path(path))
  self.palette = {kind: :git_revision_compare, query: +"", index: 0, matches: [], path: path}
end

#show_git_revision_diff(before, after, **options) ⇒ Object



81
# File 'lib/canopus/workspace/git_diff_view.rb', line 81

def show_git_revision_diff(before, after, **options) = compare_git_revisions(before, after, **options)

#show_revision_diff(before, after, **options) ⇒ Object



82
# File 'lib/canopus/workspace/git_diff_view.rb', line 82

def show_revision_diff(before, after, **options) = compare_git_revisions(before, after, **options)

#show_scm_diff(change = @scm_selection, mode: :inline, async: !!@window)) ⇒ Object

Raises:



21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
# File 'lib/canopus/workspace/git_diff_view.rb', line 21

def show_scm_diff(change = @scm_selection, mode: :inline, async: !!@window)
  sections = Workspace::GitStaging::SCM_SECTIONS
  raise Error, "Select a changed file" unless change.is_a?(Hash) && sections.key?(change[:kind])

  state = git_state
  raise Error, "Not a Git repository" unless state
  path = safe_git_diff_path(change.fetch(:path))
  mode = normalize_git_diff_mode(mode)
  schedule_git_diff(mode: mode, async: async) do
    capture = state.synchronize { |repository| capture_scm_diff(repository, path, change.fetch(:kind)) }
    snapshot, fallback, pair = materialize_scm_diff(capture)
     = (snapshot.before, snapshot.after) if snapshot.diff&.empty? && !snapshot.hunks.empty?
    unavailable = if pair.compact.empty?
      git_diff_unavailable(path, snapshot.before.text, snapshot.after.text,
        byte_values: [snapshot.before.raw, snapshot.after.raw]) || fallback
    end
    build_git_diff_source(path, pair.first, pair.last, snapshot: snapshot, metadata: ,
      diff: snapshot.diff, unavailable: unavailable)
  end
end

#toggle_git_diff_mode(current = editor, async: !!@window)) ⇒ Object Also known as: toggle_scm_diff_mode

Raises:



84
85
86
87
88
89
90
91
# File 'lib/canopus/workspace/git_diff_view.rb', line 84

def toggle_git_diff_mode(current = editor, async: !!@window)
  view = current&.buffer&.instance_variable_get(:@git_diff_view)
  raise Error, "Open a Git diff first" unless view

  mode = view.mode == :inline ? :side_by_side : :inline
  pane = @panes.find { |candidate| candidate.editors.include?(current) }
  schedule_git_diff(mode: mode, async: async, pane: pane, replacing: current) { view.source }
end