1# frozen_string_literal: true 2 3class Projects::MergeRequests::DiffsController < Projects::MergeRequests::ApplicationController 4 include DiffHelper 5 include RendersNotes 6 include Gitlab::Cache::Helpers 7 8 before_action :commit 9 before_action :define_diff_vars 10 before_action :define_diff_comment_vars, except: [:diffs_batch, :diffs_metadata] 11 before_action :update_diff_discussion_positions! 12 13 around_action :allow_gitaly_ref_name_caching 14 15 after_action :track_viewed_diffs_events, only: [:diffs_batch] 16 17 urgency :low, [ 18 :show, 19 :diff_for_path, 20 :diffs_batch, 21 :diffs_metadata 22 ] 23 24 def show 25 render_diffs 26 end 27 28 def diff_for_path 29 render_diffs 30 end 31 32 def diffs_batch 33 diff_options_hash = diff_options 34 diff_options_hash[:paths] = params[:paths] if params[:paths] 35 36 diffs = @compare.diffs_in_batch(params[:page], params[:per_page], diff_options: diff_options_hash) 37 unfoldable_positions = @merge_request.note_positions_for_paths(diffs.diff_file_paths, current_user).unfoldable 38 environment = @merge_request.environments_for(current_user, latest: true).last 39 40 diffs.unfold_diff_files(unfoldable_positions) 41 diffs.write_cache 42 43 options = { 44 environment: environment, 45 merge_request: @merge_request, 46 commit: commit, 47 diff_view: diff_view, 48 merge_ref_head_diff: render_merge_ref_head_diff?, 49 pagination_data: diffs.pagination_data, 50 allow_tree_conflicts: display_merge_conflicts_in_diff? 51 } 52 53 if diff_options_hash[:paths].blank? 54 # NOTE: Any variables that would affect the resulting json needs to be added to the cache_context to avoid stale cache issues. 55 cache_context = [ 56 current_user&.cache_key, 57 environment&.cache_key, 58 unfoldable_positions.map(&:to_h), 59 diff_view, 60 params[:w], 61 params[:expanded], 62 params[:page], 63 params[:per_page], 64 options[:merge_ref_head_diff], 65 options[:allow_tree_conflicts] 66 ] 67 68 render_cached( 69 diffs, 70 with: PaginatedDiffSerializer.new(current_user: current_user), 71 cache_context: -> (_) { [Digest::SHA256.hexdigest(cache_context.to_s)] }, 72 **options 73 ) 74 else 75 render json: PaginatedDiffSerializer.new(current_user: current_user).represent(diffs, options) 76 end 77 end 78 79 def diffs_metadata 80 diffs = @compare.diffs(diff_options) 81 82 options = additional_attributes.merge( 83 only_context_commits: show_only_context_commits?, 84 merge_ref_head_diff: render_merge_ref_head_diff?, 85 allow_tree_conflicts: display_merge_conflicts_in_diff? 86 ) 87 88 render json: DiffsMetadataSerializer.new(project: @merge_request.project, current_user: current_user) 89 .represent(diffs, options) 90 end 91 92 private 93 94 def preloadable_mr_relations 95 [{ source_project: :namespace }, { target_project: :namespace }] 96 end 97 98 # Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735 99 def render_diffs 100 diffs = @compare.diffs(diff_options) 101 @environment = @merge_request.environments_for(current_user, latest: true).last 102 103 diffs.unfold_diff_files(note_positions.unfoldable) 104 diffs.write_cache 105 106 request = { 107 current_user: current_user, 108 project: @merge_request.project, 109 render: ->(partial, locals) { view_to_html_string(partial, locals) } 110 } 111 112 options = additional_attributes.merge( 113 diff_view: "inline", 114 merge_ref_head_diff: render_merge_ref_head_diff?, 115 allow_tree_conflicts: display_merge_conflicts_in_diff? 116 ) 117 118 if @merge_request.project.context_commits_enabled? 119 options[:context_commits] = @merge_request.recent_context_commits 120 end 121 122 render json: DiffsSerializer.new(request).represent(diffs, options) 123 end 124 125 # Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735 126 def define_diff_vars 127 @merge_request_diffs = @merge_request.merge_request_diffs.viewable.order_id_desc 128 @compare = commit || find_merge_request_diff_compare 129 return render_404 unless @compare 130 end 131 132 # rubocop: disable CodeReuse/ActiveRecord 133 def commit 134 return unless commit_id = params[:commit_id].presence 135 return unless @merge_request.all_commits.exists?(sha: commit_id) || @merge_request.recent_context_commits.map(&:id).include?(commit_id) 136 137 @commit ||= @project.commit(commit_id) 138 end 139 # rubocop: enable CodeReuse/ActiveRecord 140 141 # rubocop: disable CodeReuse/ActiveRecord 142 # 143 # Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735 144 def find_merge_request_diff_compare 145 @merge_request_diff = 146 if params[:diff_id].present? 147 @merge_request.merge_request_diffs.viewable.find_by(id: params[:diff_id]) 148 else 149 @merge_request.merge_request_diff 150 end 151 152 return unless @merge_request_diff&.id 153 154 @comparable_diffs = @merge_request_diffs.select { |diff| diff.id < @merge_request_diff.id } 155 156 if @start_sha = params[:start_sha].presence 157 @start_version = @comparable_diffs.find { |diff| diff.head_commit_sha == @start_sha } 158 159 unless @start_version 160 @start_sha = @merge_request_diff.head_commit_sha 161 @start_version = @merge_request_diff 162 end 163 end 164 165 return @merge_request.context_commits_diff if show_only_context_commits? && !@merge_request.context_commits_diff.empty? 166 return @merge_request.merge_head_diff if render_merge_ref_head_diff? 167 168 if @start_sha 169 @merge_request_diff.compare_with(@start_sha) 170 else 171 @merge_request_diff 172 end 173 end 174 # rubocop: enable CodeReuse/ActiveRecord 175 176 def additional_attributes 177 { 178 environment: @environment, 179 merge_request: @merge_request, 180 merge_request_diff: @merge_request_diff, 181 merge_request_diffs: @merge_request_diffs, 182 start_version: @start_version, 183 start_sha: @start_sha, 184 commit: @commit, 185 latest_diff: @merge_request_diff&.latest? 186 } 187 end 188 189 # Deprecated: https://gitlab.com/gitlab-org/gitlab/issues/37735 190 def define_diff_comment_vars 191 @new_diff_note_attrs = { 192 noteable_type: 'MergeRequest', 193 noteable_id: @merge_request.id, 194 commit_id: @commit&.id 195 } 196 197 @diff_notes_disabled = false 198 199 @use_legacy_diff_notes = !@merge_request.has_complete_diff_refs? 200 201 @grouped_diff_discussions = @merge_request.grouped_diff_discussions(@compare.diff_refs) 202 @notes = prepare_notes_for_rendering(@grouped_diff_discussions.values.flatten.flat_map(&:notes), @merge_request) 203 end 204 205 def render_merge_ref_head_diff? 206 params[:diff_id].blank? && 207 Gitlab::Utils.to_boolean(params[:diff_head]) && 208 @merge_request.diffable_merge_ref? && 209 @start_sha.nil? 210 end 211 212 def note_positions 213 @note_positions ||= Gitlab::Diff::PositionCollection.new(renderable_notes.map(&:position)) 214 end 215 216 def renderable_notes 217 define_diff_comment_vars unless @notes 218 219 draft_notes = 220 if current_user 221 merge_request.draft_notes.authored_by(current_user) 222 else 223 [] 224 end 225 226 @notes.concat(draft_notes) 227 end 228 229 def update_diff_discussion_positions! 230 return if @merge_request.has_any_diff_note_positions? 231 232 Discussions::CaptureDiffNotePositionsService.new(@merge_request).execute 233 end 234 235 def track_viewed_diffs_events 236 return if dnt_enabled? 237 238 Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter 239 .track_mr_diffs_action(merge_request: @merge_request) 240 241 return unless current_user&.view_diffs_file_by_file 242 243 Gitlab::UsageDataCounters::MergeRequestActivityUniqueCounter 244 .track_mr_diffs_single_file_action(merge_request: @merge_request, user: current_user) 245 end 246 247 def display_merge_conflicts_in_diff? 248 Feature.enabled?(:display_merge_conflicts_in_diff, @merge_request.project) 249 end 250end 251