diff options
Diffstat (limited to 'app/controllers/concerns')
19 files changed, 446 insertions, 82 deletions
diff --git a/app/controllers/concerns/controller_with_feature_category.rb b/app/controllers/concerns/controller_with_feature_category.rb new file mode 100644 index 00000000000..f8985cf0950 --- /dev/null +++ b/app/controllers/concerns/controller_with_feature_category.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +module ControllerWithFeatureCategory + extend ActiveSupport::Concern + include Gitlab::ClassAttributes + + class_methods do + def feature_category(category, config = {}) + validate_config!(config) + + category_config = Config.new(category, config[:only], config[:except], config[:if], config[:unless]) + # Add the config to the beginning. That way, the last defined one takes precedence. + feature_category_configuration.unshift(category_config) + end + + def feature_category_for_action(action) + category_config = feature_category_configuration.find { |config| config.matches?(action) } + + category_config&.category || superclass_feature_category_for_action(action) + end + + private + + def validate_config!(config) + invalid_keys = config.keys - [:only, :except, :if, :unless] + if invalid_keys.any? + raise ArgumentError, "unknown arguments: #{invalid_keys} " + end + + if config.key?(:only) && config.key?(:except) + raise ArgumentError, "cannot configure both `only` and `except`" + end + end + + def feature_category_configuration + class_attributes[:feature_category_config] ||= [] + end + + def superclass_feature_category_for_action(action) + return unless superclass.respond_to?(:feature_category_for_action) + + superclass.feature_category_for_action(action) + end + end +end diff --git a/app/controllers/concerns/controller_with_feature_category/config.rb b/app/controllers/concerns/controller_with_feature_category/config.rb new file mode 100644 index 00000000000..624691ee4f6 --- /dev/null +++ b/app/controllers/concerns/controller_with_feature_category/config.rb @@ -0,0 +1,38 @@ +# frozen_string_literal: true + +module ControllerWithFeatureCategory + class Config + attr_reader :category + + def initialize(category, only, except, if_proc, unless_proc) + @category = category.to_sym + @only, @except = only&.map(&:to_s), except&.map(&:to_s) + @if_proc, @unless_proc = if_proc, unless_proc + end + + def matches?(action) + included?(action) && !excluded?(action) && + if_proc?(action) && !unless_proc?(action) + end + + private + + attr_reader :only, :except, :if_proc, :unless_proc + + def if_proc?(action) + if_proc.nil? || if_proc.call(action) + end + + def unless_proc?(action) + unless_proc.present? && unless_proc.call(action) + end + + def included?(action) + only.nil? || only.include?(action) + end + + def excluded?(action) + except.present? && except.include?(action) + end + end +end diff --git a/app/controllers/concerns/filters_events.rb b/app/controllers/concerns/filters_events.rb new file mode 100644 index 00000000000..c82d0318fd3 --- /dev/null +++ b/app/controllers/concerns/filters_events.rb @@ -0,0 +1,14 @@ +# frozen_string_literal: true + +module FiltersEvents + def event_filter + @event_filter ||= new_event_filter.tap { |ef| cookies[:event_filter] = ef.filter } + end + + private + + def new_event_filter + active_filter = params[:event_filter].presence || cookies[:event_filter] + EventFilter.new(active_filter) + end +end diff --git a/app/controllers/concerns/integrations_actions.rb b/app/controllers/concerns/integrations_actions.rb index cc9db7936e8..46febc44807 100644 --- a/app/controllers/concerns/integrations_actions.rb +++ b/app/controllers/concerns/integrations_actions.rb @@ -8,6 +8,9 @@ module IntegrationsActions before_action :not_found, unless: :integrations_enabled? before_action :integration, only: [:edit, :update, :test] + before_action only: :edit do + push_frontend_feature_flag(:integration_form_refactor, default_enabled: true) + end end def edit @@ -51,9 +54,8 @@ module IntegrationsActions end def integration - # Using instance variable `@service` still required as it's used in ServiceParams - # and app/views/shared/_service_settings.html.haml. Should be removed once - # those 2 are refactored to use `@integration`. + # Using instance variable `@service` still required as it's used in ServiceParams. + # Should be removed once that is refactored to use `@integration`. @integration = @service ||= find_or_initialize_integration(params[:id]) # rubocop:disable Gitlab/ModuleWithInstanceVariables end diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb index 98fa8202e25..c4dbce00593 100644 --- a/app/controllers/concerns/issuable_actions.rb +++ b/app/controllers/concerns/issuable_actions.rb @@ -110,9 +110,13 @@ module IssuableActions def bulk_update result = Issuable::BulkUpdateService.new(parent, current_user, bulk_update_params).execute(resource_name) - quantity = result[:count] - render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } + if result.success? + quantity = result.payload[:count] + render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" } + elsif result.error? + render json: { errors: result.message }, status: result.http_status + end end # rubocop:disable CodeReuse/ActiveRecord @@ -193,13 +197,13 @@ module IssuableActions def authorize_destroy_issuable! unless can?(current_user, :"destroy_#{issuable.to_ability_name}", issuable) - return access_denied! + access_denied! end end def authorize_admin_issuable! unless can?(current_user, :"admin_#{resource_name}", parent) - return access_denied! + access_denied! end end diff --git a/app/controllers/concerns/issuable_collections.rb b/app/controllers/concerns/issuable_collections.rb index 9ef067e8797..4f61e5ed711 100644 --- a/app/controllers/concerns/issuable_collections.rb +++ b/app/controllers/concerns/issuable_collections.rb @@ -81,34 +81,36 @@ module IssuableCollections # rubocop:disable Gitlab/ModuleWithInstanceVariables def finder_options - params[:state] = default_state if params[:state].blank? - - options = { - scope: params[:scope], - state: params[:state], - confidential: Gitlab::Utils.to_boolean(params[:confidential]), - sort: set_sort_order - } - - # Used by view to highlight active option - @sort = options[:sort] - - # When a user looks for an exact iid, we do not filter by search but only by iid - if params[:search] =~ /^#(?<iid>\d+)\z/ - options[:iids] = Regexp.last_match[:iid] - params[:search] = nil + strong_memoize(:finder_options) do + params[:state] = default_state if params[:state].blank? + + options = { + scope: params[:scope], + state: params[:state], + confidential: Gitlab::Utils.to_boolean(params[:confidential]), + sort: set_sort_order + } + + # Used by view to highlight active option + @sort = options[:sort] + + # When a user looks for an exact iid, we do not filter by search but only by iid + if params[:search] =~ /^#(?<iid>\d+)\z/ + options[:iids] = Regexp.last_match[:iid] + params[:search] = nil + end + + if @project + options[:project_id] = @project.id + options[:attempt_project_search_optimizations] = true + elsif @group + options[:group_id] = @group.id + options[:include_subgroups] = true + options[:attempt_group_search_optimizations] = true + end + + params.permit(finder_type.valid_params).merge(options) end - - if @project - options[:project_id] = @project.id - options[:attempt_project_search_optimizations] = true - elsif @group - options[:group_id] = @group.id - options[:include_subgroups] = true - options[:attempt_group_search_optimizations] = true - end - - params.permit(finder_type.valid_params).merge(options) end # rubocop:enable Gitlab/ModuleWithInstanceVariables @@ -147,7 +149,10 @@ module IssuableCollections when 'Issue' common_attributes + [:project, project: :namespace] when 'MergeRequest' - common_attributes + [:target_project, :latest_merge_request_diff, source_project: :route, head_pipeline: :project, target_project: :namespace] + common_attributes + [ + :target_project, :latest_merge_request_diff, :approvals, :approved_by_users, + source_project: :route, head_pipeline: :project, target_project: :namespace + ] end end # rubocop:enable Gitlab/ModuleWithInstanceVariables diff --git a/app/controllers/concerns/known_sign_in.rb b/app/controllers/concerns/known_sign_in.rb index c0b9605de58..cacc7e4628f 100644 --- a/app/controllers/concerns/known_sign_in.rb +++ b/app/controllers/concerns/known_sign_in.rb @@ -2,19 +2,34 @@ module KnownSignIn include Gitlab::Utils::StrongMemoize + include CookiesHelper + + KNOWN_SIGN_IN_COOKIE = :known_sign_in + KNOWN_SIGN_IN_COOKIE_EXPIRY = 14.days private def verify_known_sign_in - return unless current_user + return unless Gitlab::CurrentSettings.notify_on_unknown_sign_in? && current_user + + notify_user unless known_device? || known_remote_ip? - notify_user unless known_remote_ip? + update_cookie end def known_remote_ip? known_ip_addresses.include?(request.remote_ip) end + def known_device? + cookies.encrypted[KNOWN_SIGN_IN_COOKIE] == current_user.id + end + + def update_cookie + set_secure_cookie(KNOWN_SIGN_IN_COOKIE, current_user.id, + type: COOKIE_TYPE_ENCRYPTED, httponly: true, expires: KNOWN_SIGN_IN_COOKIE_EXPIRY) + end + def sessions strong_memoize(:session) do ActiveSession.list(current_user).reject(&:is_impersonated) diff --git a/app/controllers/concerns/membership_actions.rb b/app/controllers/concerns/membership_actions.rb index 4ab02005b45..8c7f156f7f8 100644 --- a/app/controllers/concerns/membership_actions.rb +++ b/app/controllers/concerns/membership_actions.rb @@ -31,7 +31,10 @@ module MembershipActions def destroy member = membershipable.members_and_requesters.find(params[:id]) - Members::DestroyService.new(current_user).execute(member) + # !! is used in case unassign_issuables contains empty string which would result in nil + unassign_issuables = !!ActiveRecord::Type::Boolean.new.cast(params.delete(:unassign_issuables)) + + Members::DestroyService.new(current_user).execute(member, unassign_issuables: unassign_issuables) respond_to do |format| format.html do diff --git a/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb new file mode 100644 index 00000000000..e0e3f628cc5 --- /dev/null +++ b/app/controllers/concerns/metrics/dashboard/prometheus_api_proxy.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Metrics::Dashboard::PrometheusApiProxy + extend ActiveSupport::Concern + include RenderServiceResults + + included do + before_action :authorize_read_prometheus!, only: [:prometheus_proxy] + end + + def prometheus_proxy + variable_substitution_result = + proxy_variable_substitution_service.new(proxyable, permit_params).execute + + if variable_substitution_result[:status] == :error + return error_response(variable_substitution_result) + end + + prometheus_result = Prometheus::ProxyService.new( + proxyable, + proxy_method, + proxy_path, + variable_substitution_result[:params] + ).execute + + return continue_polling_response if prometheus_result.nil? + return error_response(prometheus_result) if prometheus_result[:status] == :error + + success_response(prometheus_result) + end + + private + + def proxyable + raise NotImplementedError, "#{self.class} must implement method: #{__callee__}" + end + + def proxy_variable_substitution_service + raise NotImplementedError, "#{self.class} must implement method: #{__callee__}" + end + + def permit_params + params.permit! + end + + def proxy_method + request.method + end + + def proxy_path + params[:proxy_path] + end +end diff --git a/app/controllers/concerns/metrics_dashboard.rb b/app/controllers/concerns/metrics_dashboard.rb index 1aea0e294a5..28d0692d748 100644 --- a/app/controllers/concerns/metrics_dashboard.rb +++ b/app/controllers/concerns/metrics_dashboard.rb @@ -13,7 +13,7 @@ module MetricsDashboard result = dashboard_finder.find( project_for_dashboard, current_user, - metrics_dashboard_params.to_h.symbolize_keys + decoded_params ) if result @@ -41,7 +41,7 @@ module MetricsDashboard end def amend_dashboard(dashboard) - project_dashboard = project_for_dashboard && !dashboard[:system_dashboard] + project_dashboard = project_for_dashboard && !dashboard[:out_of_the_box_dashboard] dashboard[:can_edit] = project_dashboard ? can_edit?(dashboard) : false dashboard[:project_blob_path] = project_dashboard ? dashboard_project_blob_path(dashboard) : nil @@ -114,4 +114,14 @@ module MetricsDashboard json: result.slice(:all_dashboards, :message, :status) } end + + def decoded_params + params = metrics_dashboard_params + + if params[:dashboard_path] + params[:dashboard_path] = CGI.unescape(params[:dashboard_path]) + end + + params + end end diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb index d3dfb1813e4..f4fc7decb60 100644 --- a/app/controllers/concerns/notes_actions.rb +++ b/app/controllers/concerns/notes_actions.rb @@ -5,6 +5,11 @@ module NotesActions include Gitlab::Utils::StrongMemoize extend ActiveSupport::Concern + # last_fetched_at is an integer number of microseconds, which is the same + # precision as PostgreSQL "timestamp" fields. It's important for them to have + # identical precision for accurate pagination + MICROSECOND = 1_000_000 + included do before_action :set_polling_interval_header, only: [:index] before_action :require_noteable!, only: [:index, :create] @@ -13,30 +18,20 @@ module NotesActions end def index - notes_json = { notes: [], last_fetched_at: Time.current.to_i } - - notes = notes_finder - .execute - .inc_relations_for_view - - if notes_filter != UserPreference::NOTES_FILTERS[:only_comments] - notes = - ResourceEvents::MergeIntoNotesService - .new(noteable, current_user, last_fetched_at: last_fetched_at) - .execute(notes) - end - + notes, meta = gather_notes notes = prepare_notes_for_rendering(notes) notes = notes.select { |n| n.readable_by?(current_user) } - - notes_json[:notes] = + notes = if use_note_serializer? note_serializer.represent(notes) else notes.map { |note| note_json(note) } end - render json: notes_json + # We know there's more data, so tell the frontend to poll again after 1ms + set_polling_interval_header(interval: 1) if meta[:more] + + render json: meta.merge(notes: notes) end # rubocop:disable Gitlab/ModuleWithInstanceVariables @@ -101,6 +96,48 @@ module NotesActions private + # Lower bound (last_fetched_at as specified in the request) is already set in + # the finder. Here, we select between returning all notes since then, or a + # page's worth of notes. + def gather_notes + if Feature.enabled?(:paginated_notes, project) + gather_some_notes + else + gather_all_notes + end + end + + def gather_all_notes + now = Time.current + notes = merge_resource_events(notes_finder.execute.inc_relations_for_view) + + [notes, { last_fetched_at: (now.to_i * MICROSECOND) + now.usec }] + end + + def gather_some_notes + paginator = Gitlab::UpdatedNotesPaginator.new( + notes_finder.execute.inc_relations_for_view, + last_fetched_at: last_fetched_at + ) + + notes = paginator.notes + + # Fetch all the synthetic notes in the same time range as the real notes. + # Although we don't limit the number, their text is under our control so + # should be fairly cheap to process. + notes = merge_resource_events(notes, fetch_until: paginator.next_fetched_at) + + [notes, paginator.metadata] + end + + def merge_resource_events(notes, fetch_until: nil) + return notes if notes_filter == UserPreference::NOTES_FILTERS[:only_comments] + + ResourceEvents::MergeIntoNotesService + .new(noteable, current_user, last_fetched_at: last_fetched_at, fetch_until: fetch_until) + .execute(notes) + end + def note_html(note) render_to_string( "shared/notes/_note", @@ -226,11 +263,11 @@ module NotesActions end def update_note_params - params.require(:note).permit(:note) + params.require(:note).permit(:note, :position) end - def set_polling_interval_header - Gitlab::PollingInterval.set_header(response, interval: 6_000) + def set_polling_interval_header(interval: 6000) + Gitlab::PollingInterval.set_header(response, interval: interval) end def noteable @@ -242,7 +279,14 @@ module NotesActions end def last_fetched_at - request.headers['X-Last-Fetched-At'] + strong_memoize(:last_fetched_at) do + microseconds = request.headers['X-Last-Fetched-At'].to_i + + seconds = microseconds / MICROSECOND + frac = microseconds % MICROSECOND + + Time.zone.at(seconds, frac) + end end def notes_filter diff --git a/app/controllers/concerns/renders_member_access.rb b/app/controllers/concerns/renders_member_access.rb index 955ac1a1bc8..745830181c1 100644 --- a/app/controllers/concerns/renders_member_access.rb +++ b/app/controllers/concerns/renders_member_access.rb @@ -7,12 +7,6 @@ module RendersMemberAccess groups end - def prepare_projects_for_rendering(projects) - preload_max_member_access_for_collection(Project, projects) - - projects - end - private # rubocop: disable CodeReuse/ActiveRecord diff --git a/app/controllers/concerns/renders_projects_list.rb b/app/controllers/concerns/renders_projects_list.rb new file mode 100644 index 00000000000..be45c676ad6 --- /dev/null +++ b/app/controllers/concerns/renders_projects_list.rb @@ -0,0 +1,13 @@ +# frozen_string_literal: true + +module RendersProjectsList + def prepare_projects_for_rendering(projects) + preload_max_member_access_for_collection(Project, projects) + + # Call the forks count method on every project, so the BatchLoader would load them all at + # once when the entities are rendered + projects.each(&:forks_count) + + projects + end +end diff --git a/app/controllers/concerns/service_params.rb b/app/controllers/concerns/service_params.rb index e78fa8f8250..a19c43a227a 100644 --- a/app/controllers/concerns/service_params.rb +++ b/app/controllers/concerns/service_params.rb @@ -22,8 +22,8 @@ module ServiceParams :comment_on_event_enabled, :comment_detail, :confidential_issues_events, + :confluence_url, :default_irc_uri, - :description, :device, :disable_diffs, :drone_url, @@ -31,6 +31,7 @@ module ServiceParams :external_wiki_url, :google_iap_service_account_json, :google_iap_audience_client_id, + :inherit_from_id, # We're using `issues_events` and `merge_requests_events` # in the view so we still need to explicitly state them # here. `Service#event_names` would only give @@ -61,7 +62,6 @@ module ServiceParams :sound, :subdomain, :teamcity_url, - :title, :token, :type, :url, diff --git a/app/controllers/concerns/snippets/blobs_actions.rb b/app/controllers/concerns/snippets/blobs_actions.rb new file mode 100644 index 00000000000..db56ce8f193 --- /dev/null +++ b/app/controllers/concerns/snippets/blobs_actions.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Snippets::BlobsActions + extend ActiveSupport::Concern + + include Gitlab::Utils::StrongMemoize + include ExtractsRef + include Snippets::SendBlob + + included do + before_action :authorize_read_snippet!, only: [:raw] + before_action :ensure_repository + before_action :ensure_blob + end + + def raw + send_snippet_blob(snippet, blob) + end + + private + + def repository_container + snippet + end + + # rubocop:disable Gitlab/ModuleWithInstanceVariables + def blob + strong_memoize(:blob) do + assign_ref_vars + + next unless @commit + + @repo.blob_at(@commit.id, @path) + end + end + # rubocop:enable Gitlab/ModuleWithInstanceVariables + + def ensure_blob + render_404 unless blob + end + + def ensure_repository + unless snippet.repo_exists? + Gitlab::AppLogger.error(message: "Snippet raw blob attempt with no repo", snippet: snippet.id) + + respond_422 + end + end + + def snippet_id + params[:snippet_id] + end +end diff --git a/app/controllers/concerns/snippets/send_blob.rb b/app/controllers/concerns/snippets/send_blob.rb new file mode 100644 index 00000000000..4f432430aaa --- /dev/null +++ b/app/controllers/concerns/snippets/send_blob.rb @@ -0,0 +1,22 @@ +# frozen_string_literal: true + +module Snippets::SendBlob + include SendsBlob + + def send_snippet_blob(snippet, blob) + workhorse_set_content_type! + + send_blob( + snippet.repository, + blob, + inline: content_disposition == 'inline', + allow_caching: snippet.public? + ) + end + + private + + def content_disposition + @disposition ||= params[:inline] == 'false' ? 'attachment' : 'inline' + end +end diff --git a/app/controllers/concerns/snippets_actions.rb b/app/controllers/concerns/snippets_actions.rb index 51fc12398d9..048b18c5c61 100644 --- a/app/controllers/concerns/snippets_actions.rb +++ b/app/controllers/concerns/snippets_actions.rb @@ -2,11 +2,13 @@ module SnippetsActions extend ActiveSupport::Concern - include SendsBlob + include RendersNotes include RendersBlob include PaginatedCollection include Gitlab::NoteableMetadata + include Snippets::SendBlob + include SnippetsSort included do skip_before_action :verify_authenticity_token, @@ -25,6 +27,10 @@ module SnippetsActions render 'edit' end + # This endpoint is being replaced by Snippets::BlobController#raw + # Support for old raw links will be maintainted via this action but + # it will only return the first blob found, + # see: https://gitlab.com/gitlab-org/gitlab/-/issues/217775 def raw workhorse_set_content_type! @@ -39,12 +45,7 @@ module SnippetsActions filename: Snippet.sanitized_file_name(blob.name) ) else - send_blob( - snippet.repository, - blob, - inline: content_disposition == 'inline', - allow_caching: snippet.public? - ) + send_snippet_blob(snippet, blob) end end @@ -106,10 +107,6 @@ module SnippetsActions private - def content_disposition - @disposition ||= params[:inline] == 'false' ? 'attachment' : 'inline' - end - # rubocop:disable Gitlab/ModuleWithInstanceVariables def blob return unless snippet diff --git a/app/controllers/concerns/snippets_sort.rb b/app/controllers/concerns/snippets_sort.rb new file mode 100644 index 00000000000..f122c843af7 --- /dev/null +++ b/app/controllers/concerns/snippets_sort.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +module SnippetsSort + extend ActiveSupport::Concern + + def sort_param + params[:sort].presence || 'updated_desc' + end +end diff --git a/app/controllers/concerns/wiki_actions.rb b/app/controllers/concerns/wiki_actions.rb index 7eef12fadfe..a5182000f5b 100644 --- a/app/controllers/concerns/wiki_actions.rb +++ b/app/controllers/concerns/wiki_actions.rb @@ -1,6 +1,8 @@ # frozen_string_literal: true module WikiActions + include DiffHelper + include PreviewMarkdown include SendsBlob include Gitlab::Utils::StrongMemoize extend ActiveSupport::Concern @@ -11,16 +13,23 @@ module WikiActions before_action :authorize_admin_wiki!, only: :destroy before_action :wiki - before_action :page, only: [:show, :edit, :update, :history, :destroy] + before_action :page, only: [:show, :edit, :update, :history, :destroy, :diff] before_action :load_sidebar, except: [:pages] + before_action :set_content_class before_action only: [:show, :edit, :update] do @valid_encoding = valid_encoding? end before_action only: [:edit, :update], unless: :valid_encoding? do - redirect_to wiki_page_path(wiki, page) + if params[:id].present? + redirect_to wiki_page_path(wiki, page || params[:id]) + else + redirect_to wiki_path(wiki) + end end + + helper_method :view_file_button, :diff_file_html_data end def new @@ -133,6 +142,19 @@ module WikiActions # rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables + def diff + return render_404 unless page + + apply_diff_view_cookie! + + @diffs = page.diffs(diff_options) + @diff_notes_disabled = true + + render 'shared/wikis/diff' + end + # rubocop:disable Gitlab/ModuleWithInstanceVariables + + # rubocop:disable Gitlab/ModuleWithInstanceVariables def destroy WikiPages::DestroyService.new(container: container, current_user: current_user).execute(page) @@ -203,7 +225,7 @@ module WikiActions def page_params keys = [:id] - keys << :version_id if params[:action] == 'show' + keys << :version_id if %w[show diff].include?(params[:action]) params.values_at(*keys) end @@ -229,4 +251,25 @@ module WikiActions wiki.repository.blob_at(commit.id, params[:id]) end end + + def set_content_class + @content_class = 'limit-container-width' unless fluid_layout # rubocop:disable Gitlab/ModuleWithInstanceVariables + end + + # Override CommitsHelper#view_file_button + def view_file_button(commit_sha, *args) + path = wiki_page_path(wiki, page, version_id: page.version.id) + + helpers.link_to(path, class: 'btn') do + helpers.raw(_('View page @ ')) + helpers.content_tag(:span, Commit.truncate_sha(commit_sha), class: 'commit-sha') + end + end + + # Override DiffHelper#diff_file_html_data + def diff_file_html_data(_project, _diff_file_path, diff_commit_id) + { + blob_diff_path: wiki_page_path(wiki, page, action: :diff, version_id: diff_commit_id), + view: diff_view + } + end end |