GitLab wurde aktualisiert. Dank regelmäßiger Updates bleibt das THM GitLab sicher und Sie profitieren von den neuesten Funktionen. Vielen Dank für Ihre Geduld.

Commit cd7c2cb6 authored by Paco Guzman's avatar Paco Guzman
Browse files

Cache highlighted diff lines for merge requests

Introducing the concept of SafeDiffs which relates 
diffs with UI highlighting.
parent 195b20e1
...@@ -10,6 +10,7 @@ v 8.11.0 (unreleased) ...@@ -10,6 +10,7 @@ v 8.11.0 (unreleased)
- The Repository class is now instrumented - The Repository class is now instrumented
- Cache the commit author in RequestStore to avoid extra lookups in PostReceive - Cache the commit author in RequestStore to avoid extra lookups in PostReceive
- Expand commit message width in repo view (ClemMakesApps) - Expand commit message width in repo view (ClemMakesApps)
- Cache highlighted diff lines for merge requests
- Fix of 'Commits being passed to custom hooks are already reachable when using the UI' - Fix of 'Commits being passed to custom hooks are already reachable when using the UI'
- Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable - Add support for using RequestStore within Sidekiq tasks via SIDEKIQ_REQUEST_STORE env variable
- Optimize maximum user access level lookup in loading of notes - Optimize maximum user access level lookup in loading of notes
......
module DiffForPath module DiffForPath
extend ActiveSupport::Concern extend ActiveSupport::Concern
def render_diff_for_path(diffs, diff_refs, project) def render_diff_for_path(diffs)
diff_file = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository).find do |diff| diff_file = diffs.diff_files.find do |diff|
diff.old_path == params[:old_path] && diff.new_path == params[:new_path] diff.old_path == params[:old_path] && diff.new_path == params[:new_path]
end end
...@@ -14,7 +14,7 @@ def render_diff_for_path(diffs, diff_refs, project) ...@@ -14,7 +14,7 @@ def render_diff_for_path(diffs, diff_refs, project)
locals = { locals = {
diff_file: diff_file, diff_file: diff_file,
diff_commit: diff_commit, diff_commit: diff_commit,
diff_refs: diff_refs, diff_refs: diffs.diff_refs,
blob: blob, blob: blob,
project: project project: project
} }
......
...@@ -28,7 +28,7 @@ def show ...@@ -28,7 +28,7 @@ def show
end end
def diff_for_path def diff_for_path
render_diff_for_path(@diffs, @commit.diff_refs, @project) render_diff_for_path(SafeDiffs::Commit.new(@commit, diff_options: diff_options))
end end
def builds def builds
...@@ -110,7 +110,7 @@ def define_commit_vars ...@@ -110,7 +110,7 @@ def define_commit_vars
opts = diff_options opts = diff_options
opts[:ignore_whitespace_change] = true if params[:format] == 'diff' opts[:ignore_whitespace_change] = true if params[:format] == 'diff'
@diffs = commit.diffs(opts) @diffs = SafeDiffs::Commit.new(commit, diff_options: opts)
@notes_count = commit.notes.count @notes_count = commit.notes.count
end end
......
...@@ -21,7 +21,7 @@ def show ...@@ -21,7 +21,7 @@ def show
def diff_for_path def diff_for_path
return render_404 unless @compare return render_404 unless @compare
render_diff_for_path(@diffs, @diff_refs, @project) render_diff_for_path(SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options))
end end
def create def create
...@@ -46,12 +46,12 @@ def define_diff_vars ...@@ -46,12 +46,12 @@ def define_diff_vars
@commit = @project.commit(@head_ref) @commit = @project.commit(@head_ref)
@base_commit = @project.merge_base_commit(@start_ref, @head_ref) @base_commit = @project.merge_base_commit(@start_ref, @head_ref)
@diffs = @compare.diffs(diff_options) diff_refs = Gitlab::Diff::DiffRefs.new(
@diff_refs = Gitlab::Diff::DiffRefs.new(
base_sha: @base_commit.try(:sha), base_sha: @base_commit.try(:sha),
start_sha: @start_commit.try(:sha), start_sha: @start_commit.try(:sha),
head_sha: @commit.try(:sha) head_sha: @commit.try(:sha)
) )
@diffs = SafeDiffs::Compare.new(@compare, project: @project, diff_options: diff_options, diff_refs: diff_refs)
@diff_notes_disabled = true @diff_notes_disabled = true
@grouped_diff_discussions = {} @grouped_diff_discussions = {}
......
...@@ -103,9 +103,8 @@ def diff_for_path ...@@ -103,9 +103,8 @@ def diff_for_path
end end
define_commit_vars define_commit_vars
diffs = @merge_request.diffs(diff_options)
render_diff_for_path(diffs, @merge_request.diff_refs, @merge_request.project) render_diff_for_path(SafeDiffs::MergeRequest.new(merge_request, diff_options: diff_options))
end end
def commits def commits
...@@ -153,7 +152,12 @@ def new ...@@ -153,7 +152,12 @@ def new
@commits = @merge_request.compare_commits.reverse @commits = @merge_request.compare_commits.reverse
@commit = @merge_request.diff_head_commit @commit = @merge_request.diff_head_commit
@base_commit = @merge_request.diff_base_commit @base_commit = @merge_request.diff_base_commit
@diffs = @merge_request.compare.diffs(diff_options) if @merge_request.compare if @merge_request.compare
@diffs = SafeDiffs::Compare.new(@merge_request.compare,
project: @merge_request.project,
diff_refs: @merge_request.diff_refs,
diff_options: diff_options)
end
@diff_notes_disabled = true @diff_notes_disabled = true
@pipeline = @merge_request.pipeline @pipeline = @merge_request.pipeline
......
...@@ -206,10 +206,10 @@ def commit_person_link(commit, options = {}) ...@@ -206,10 +206,10 @@ def commit_person_link(commit, options = {})
end end
end end
def view_file_btn(commit_sha, diff, project) def view_file_btn(commit_sha, diff_new_path, project)
link_to( link_to(
namespace_project_blob_path(project.namespace, project, namespace_project_blob_path(project.namespace, project,
tree_join(commit_sha, diff.new_path)), tree_join(commit_sha, diff_new_path)),
class: 'btn view-file js-view-file btn-file-option' class: 'btn view-file js-view-file btn-file-option'
) do ) do
raw('View file @') + content_tag(:span, commit_sha[0..6], raw('View file @') + content_tag(:span, commit_sha[0..6],
......
...@@ -23,18 +23,17 @@ def diff_view ...@@ -23,18 +23,17 @@ def diff_view
end end
def diff_options def diff_options
options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? } options = SafeDiffs.default_options.merge(
ignore_whitespace_change: hide_whitespace?,
no_collapse: expand_all_diffs?
)
if action_name == 'diff_for_path' if action_name == 'diff_for_path'
options[:no_collapse] = true options[:no_collapse] = true
options[:paths] = params.values_at(:old_path, :new_path) options[:paths] = params.values_at(:old_path, :new_path)
end end
Commit.max_diff_options.merge(options) options
end
def safe_diff_files(diffs, diff_refs: nil, repository: nil)
diffs.decorate! { |diff| Gitlab::Diff::File.new(diff, diff_refs: diff_refs, repository: repository) }
end end
def unfold_bottom_class(bottom) def unfold_bottom_class(bottom)
......
...@@ -313,6 +313,8 @@ def reload_diff ...@@ -313,6 +313,8 @@ def reload_diff
merge_request_diff.reload_content merge_request_diff.reload_content
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs new_diff_refs = self.diff_refs
update_diff_notes_positions( update_diff_notes_positions(
......
module SafeDiffs
def self.default_options
::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false)
end
end
module SafeDiffs
class Base
attr_reader :project, :diff_options, :diff_view, :diff_refs
delegate :count, :real_size, to: :diff_files
def initialize(diffs, project:, diff_options:, diff_refs: nil)
@diffs = diffs
@project = project
@diff_options = diff_options
@diff_refs = diff_refs
end
def diff_files
@diff_files ||= begin
diffs = @diffs.decorate! do |diff|
Gitlab::Diff::File.new(diff, diff_refs: @diff_refs, repository: @project.repository)
end
highlight!(diffs)
diffs
end
end
private
def highlight!(diff_files)
if cacheable?
cache_highlight!(diff_files)
else
diff_files.each { |diff_file| highlight_diff_file!(diff_file) }
end
end
def cacheable?
false
end
def cache_highlight!
raise NotImplementedError
end
def highlight_diff_file_from_cache!(diff_file, cache_diff_lines)
diff_file.diff_lines = cache_diff_lines.map do |line|
Gitlab::Diff::Line.init_from_hash(line)
end
end
def highlight_diff_file!(diff_file)
diff_file.diff_lines = Gitlab::Diff::Highlight.new(diff_file, repository: diff_file.repository).highlight
diff_file.highlighted_diff_lines = diff_file.diff_lines # To be used on parallel diff
diff_file
end
end
end
module SafeDiffs
class Commit < Base
def initialize(commit, diff_options:)
super(commit.diffs(diff_options),
project: commit.project,
diff_options: diff_options,
diff_refs: commit.diff_refs)
end
end
end
module SafeDiffs
class Compare < Base
def initialize(compare, project:, diff_options:, diff_refs: nil)
super(compare.diffs(diff_options),
project: project,
diff_options: diff_options,
diff_refs: diff_refs)
end
end
end
module SafeDiffs
class MergeRequest < Base
def initialize(merge_request, diff_options:)
@merge_request = merge_request
super(merge_request.diffs(diff_options),
project: merge_request.project,
diff_options: diff_options,
diff_refs: merge_request.diff_refs)
end
private
#
# If we find the highlighted diff files lines on the cache we replace existing diff_files lines (no highlighted)
# for the highlighted ones, so we just skip their execution.
# If the highlighted diff files lines are not cached we calculate and cache them.
#
# The content of the cache is and Hash where the key correspond to the file_path and the values are Arrays of
# hashes than represent serialized diff lines.
#
def cache_highlight!(diff_files)
highlighted_cache = Rails.cache.read(cache_key) || {}
highlighted_cache_was_empty = highlighted_cache.empty?
diff_files.each do |diff_file|
file_path = diff_file.file_path
if highlighted_cache[file_path]
highlight_diff_file_from_cache!(diff_file, highlighted_cache[file_path])
else
highlight_diff_file!(diff_file)
highlighted_cache[file_path] = diff_file.diff_lines.map(&:to_hash)
end
end
if highlighted_cache_was_empty
Rails.cache.write(cache_key, highlighted_cache)
end
diff_files
end
def cacheable?
@merge_request.merge_request_diff.present?
end
def cache_key
[@merge_request.merge_request_diff, 'highlighted-safe-diff-files', diff_options]
end
end
end
module MergeRequests
class MergeRequestDiffCacheService
def execute(merge_request)
# Executing the iteration we cache all the highlighted diff information
SafeDiffs::MergeRequest.new(merge_request, diff_options: SafeDiffs.default_options).diff_files.to_a
end
end
end
...@@ -75,7 +75,7 @@ ...@@ -75,7 +75,7 @@
- blob = diff_file.blob - blob = diff_file.blob
- if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob) - if blob && blob.respond_to?(:text?) && blob_text_viewable?(blob)
%table.code.white %table.code.white
- diff_file.highlighted_diff_lines.each do |line| - diff_file.diff_lines.each do |line|
= render "projects/diffs/line", line: line, diff_file: diff_file, plain: true = render "projects/diffs/line", line: line, diff_file: diff_file, plain: true
- else - else
No preview for this file type No preview for this file type
......
...@@ -7,7 +7,7 @@ ...@@ -7,7 +7,7 @@
= render "ci_menu" = render "ci_menu"
- else - else
%div.block-connector %div.block-connector
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @commit.diff_refs = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs
= render "projects/notes/notes_with_form" = render "projects/notes/notes_with_form"
- if can_collaborate_with_project? - if can_collaborate_with_project?
- %w(revert cherry-pick).each do |type| - %w(revert cherry-pick).each do |type|
......
...@@ -8,7 +8,7 @@ ...@@ -8,7 +8,7 @@
- if @commits.present? - if @commits.present?
= render "projects/commits/commit_list" = render "projects/commits/commit_list"
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @diff_refs = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @diffs.diff_refs
- else - else
.light-well .light-well
.center .center
......
...@@ -2,8 +2,6 @@ ...@@ -2,8 +2,6 @@
- if diff_view == 'parallel' - if diff_view == 'parallel'
- fluid_layout true - fluid_layout true
- diff_files = safe_diff_files(diffs, diff_refs: diff_refs, repository: project.repository)
.content-block.oneline-block.files-changed .content-block.oneline-block.files-changed
.inline-parallel-buttons .inline-parallel-buttons
- if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? } - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? }
......
...@@ -15,6 +15,6 @@ ...@@ -15,6 +15,6 @@
from_merge_request_id: @merge_request.id, from_merge_request_id: @merge_request.id,
skip_visible_check: true) skip_visible_check: true)
= view_file_btn(diff_commit.id, diff_file, project) = view_file_btn(diff_commit.id, diff_file.new_path, project)
= render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, diff_refs: diff_refs, blob: blob, project: project = render 'projects/diffs/content', diff_file: diff_file, diff_commit: diff_commit, blob: blob, project: project
...@@ -5,7 +5,7 @@ ...@@ -5,7 +5,7 @@
%table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' } %table.text-file.code.js-syntax-highlight{ data: diff_view_data, class: too_big ? 'hide' : '' }
- last_line = 0 - last_line = 0
- diff_file.highlighted_diff_lines.each do |line| - diff_file.diff_lines.each do |line|
- last_line = line.new_pos - last_line = line.new_pos
= render "projects/diffs/line", line: line, diff_file: diff_file = render "projects/diffs/line", line: line, diff_file: diff_file
......
...@@ -42,7 +42,7 @@ ...@@ -42,7 +42,7 @@
%h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits. %h4 This comparison includes more than #{MergeRequestDiff::COMMITS_SAFE_SIZE} commits.
%p To preserve performance the line changes are not shown. %p To preserve performance the line changes are not shown.
- else - else
= render "projects/diffs/diffs", diffs: @diffs, project: @project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false = render "projects/diffs/diffs", diff_files: @diffs.diff_files, project: @diffs.project, diff_refs: @merge_request.diff_refs, show_whitespace_toggle: false
- if @pipeline - if @pipeline
#builds.builds.tab-pane #builds.builds.tab-pane
= render "projects/merge_requests/show/builds" = render "projects/merge_requests/show/builds"
......
- if @merge_request_diff.collected? - if @merge_request_diff.collected?
= render "projects/diffs/diffs", diffs: @merge_request.diffs(diff_options), - diffs = SafeDiffs::MergeRequest.new(@merge_request, diff_options: diff_options)
project: @merge_request.project, diff_refs: @merge_request.diff_refs = render "projects/diffs/diffs", diff_files: diffs.diff_files,
diff_refs: diffs.diff_refs, project: diffs.project
- elsif @merge_request_diff.empty? - elsif @merge_request_diff.empty?
.nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch} .nothing-here-block Nothing to merge from #{@merge_request.source_branch} into #{@merge_request.target_branch}
- else - else
......
...@@ -63,15 +63,18 @@ def new_ref ...@@ -63,15 +63,18 @@ def new_ref
diff_refs.try(:head_sha) diff_refs.try(:head_sha)
end end
attr_writer :diff_lines, :highlighted_diff_lines
# Array of Gitlab::Diff::Line objects # Array of Gitlab::Diff::Line objects
def diff_lines def diff_lines
@lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a @diff_lines ||= Gitlab::Diff::Parser.new.parse(raw_diff.each_line).to_a
end end
def highlighted_diff_lines def highlighted_diff_lines
@highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight @highlighted_diff_lines ||= Gitlab::Diff::Highlight.new(self, repository: self.repository).highlight
end end
# Array[<Hash>] with right/left keys that contains Gitlab::Diff::Line objects which text is hightlighted
def parallel_diff_lines def parallel_diff_lines
@parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize @parallel_diff_lines ||= Gitlab::Diff::ParallelDiff.new(self).parallelize
end end
......
...@@ -40,8 +40,6 @@ def highlight ...@@ -40,8 +40,6 @@ def highlight
def highlight_line(diff_line) def highlight_line(diff_line)
return unless diff_file && diff_file.diff_refs return unless diff_file && diff_file.diff_refs
line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
rich_line = rich_line =
if diff_line.unchanged? || diff_line.added? if diff_line.unchanged? || diff_line.added?
new_lines[diff_line.new_pos - 1] new_lines[diff_line.new_pos - 1]
...@@ -51,7 +49,10 @@ def highlight_line(diff_line) ...@@ -51,7 +49,10 @@ def highlight_line(diff_line)
# Only update text if line is found. This will prevent # Only update text if line is found. This will prevent
# issues with submodules given the line only exists in diff content. # issues with submodules given the line only exists in diff content.
"#{line_prefix}#{rich_line}".html_safe if rich_line if rich_line
line_prefix = diff_line.text.match(/\A(.)/) ? $1 : ' '
"#{line_prefix}#{rich_line}".html_safe
end
end end
def inline_diffs def inline_diffs
......
...@@ -9,6 +9,20 @@ def initialize(text, type, index, old_pos, new_pos) ...@@ -9,6 +9,20 @@ def initialize(text, type, index, old_pos, new_pos)
@old_pos, @new_pos = old_pos, new_pos @old_pos, @new_pos = old_pos, new_pos
end end
def self.init_from_hash(hash)
new(hash[:text], hash[:type], hash[:index], hash[:old_pos], hash[:new_pos])
end
def serialize_keys
@serialize_keys ||= %i(text type index old_pos new_pos)
end
def to_hash
hash = {}
serialize_keys.each { |key| hash[key] = send(key) }
hash
end
def old_line def old_line
old_pos unless added? || meta? old_pos unless added? || meta?
end end
......
...@@ -41,7 +41,7 @@ def commits ...@@ -41,7 +41,7 @@ def commits
def diffs def diffs
return unless compare return unless compare
@diffs ||= safe_diff_files(compare.diffs(max_files: 30), diff_refs: diff_refs, repository: project.repository) @diffs ||= SafeDiffs::Compare.new(compare, diff_options: { max_files: 30 }, project: project, diff_refs: diff_refs).diff_files
end end
def diffs_count def diffs_count
......
...@@ -83,7 +83,7 @@ def go(extra_params = {}) ...@@ -83,7 +83,7 @@ def go(extra_params = {})
let(:format) { :diff } let(:format) { :diff }
it "should really only be a git diff" do it "should really only be a git diff" do
go(id: commit.id, format: format) go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format)
expect(response.body).to start_with("diff --git") expect(response.body).to start_with("diff --git")
end end
...@@ -92,8 +92,9 @@ def go(extra_params = {}) ...@@ -92,8 +92,9 @@ def go(extra_params = {})
go(id: '66eceea0db202bb39c4e445e8ca28689645366c5', format: format, w: 1) go(id: </