1# frozen_string_literal: true 2 3# Controller for viewing a file's blame 4class Projects::BlobController < Projects::ApplicationController 5 include ExtractsPath 6 include CreatesCommit 7 include RendersBlob 8 include NotesHelper 9 include ActionView::Helpers::SanitizeHelper 10 include RedirectsForMissingPathOnTree 11 include SourcegraphDecorator 12 include DiffHelper 13 include RedisTracking 14 extend ::Gitlab::Utils::Override 15 16 prepend_before_action :authenticate_user!, only: [:edit] 17 18 around_action :allow_gitaly_ref_name_caching, only: [:show] 19 20 before_action :require_non_empty_project, except: [:new, :create] 21 before_action :authorize_download_code! 22 23 # We need to assign the blob vars before `authorize_edit_tree!` so we can 24 # validate access to a specific ref. 25 before_action :assign_blob_vars 26 27 # Since BlobController doesn't use assign_ref_vars, we have to call this explicitly 28 before_action :rectify_renamed_default_branch!, only: [:show] 29 30 before_action :authorize_edit_tree!, only: [:new, :create, :update, :destroy] 31 32 before_action :commit, except: [:new, :create] 33 before_action :blob, except: [:new, :create] 34 before_action :require_branch_head, only: [:edit, :update] 35 before_action :editor_variables, except: [:show, :preview, :diff] 36 before_action :validate_diff_params, only: :diff 37 before_action :set_last_commit_sha, only: [:edit, :update] 38 before_action :track_experiment, only: :create 39 40 track_redis_hll_event :create, :update, name: 'g_edit_by_sfe' 41 42 feature_category :source_code_management 43 urgency :low, [:create, :show, :edit, :update, :diff] 44 45 before_action do 46 push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml) 47 push_frontend_feature_flag(:highlight_js, @project, default_enabled: :yaml) 48 push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml) 49 push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks) 50 end 51 52 def new 53 commit unless @repository.empty? 54 end 55 56 def create 57 create_commit(Files::CreateService, success_notice: _("The file has been successfully created."), 58 success_path: -> { create_success_path }, 59 failure_view: :new, 60 failure_path: project_new_blob_path(@project, @ref)) 61 end 62 63 def show 64 conditionally_expand_blob(@blob) 65 66 respond_to do |format| 67 format.html do 68 show_html 69 end 70 71 format.json do 72 page_title @blob.path, @ref, @project.full_name 73 74 show_json 75 end 76 end 77 end 78 79 def edit 80 if can_collaborate_with_project?(project, ref: @ref) 81 blob.load_all_data! 82 else 83 redirect_to action: 'show' 84 end 85 end 86 87 def update 88 @path = params[:file_path] if params[:file_path].present? 89 90 create_commit(Files::UpdateService, success_path: -> { after_edit_path }, 91 failure_view: :edit, 92 failure_path: project_blob_path(@project, @id)) 93 rescue Files::UpdateService::FileChangedError 94 @conflict = true 95 render :edit 96 end 97 98 def preview 99 @content = params[:content] 100 @blob.load_all_data! 101 diffy = Diffy::Diff.new(@blob.data, @content, diff: '-U 3', include_diff_info: true) 102 diff_lines = diffy.diff.scan(/.*\n/)[2..] 103 diff_lines = Gitlab::Diff::Parser.new.parse(diff_lines).to_a 104 @diff_lines = Gitlab::Diff::Highlight.new(diff_lines, repository: @repository).highlight 105 106 render layout: false 107 end 108 109 def destroy 110 create_commit(Files::DeleteService, success_notice: _("The file has been successfully deleted."), 111 success_path: -> { after_delete_path }, 112 failure_path: project_blob_path(@project, @id)) 113 end 114 115 def diff 116 @form = Blobs::UnfoldPresenter.new(blob, diff_params) 117 118 # keep only json rendering when 119 # https://gitlab.com/gitlab-org/gitlab-foss/issues/44988 is done 120 if rendered_for_merge_request? 121 render json: DiffLineSerializer.new.represent(@form.diff_lines) 122 else 123 @lines = @form.lines 124 @match_line = @form.match_line_text 125 render layout: false 126 end 127 end 128 129 private 130 131 attr_reader :branch_name 132 133 def blob 134 @blob ||= @repository.blob_at(@commit.id, @path) 135 136 if @blob 137 @blob 138 else 139 if tree = @repository.tree(@commit.id, @path) 140 if tree.entries.any? 141 return redirect_to project_tree_path(@project, File.join(@ref, @path)) 142 end 143 end 144 145 redirect_to_tree_root_for_missing_path(@project, @ref, @path) 146 end 147 end 148 149 def commit 150 @commit ||= @repository.commit(@ref) 151 152 return render_404 unless @commit 153 end 154 155 def redirect_renamed_default_branch? 156 action_name == 'show' 157 end 158 159 def assign_blob_vars 160 @id = params[:id] 161 @ref, @path = extract_ref(@id) 162 rescue InvalidPathError 163 render_404 164 end 165 166 def rectify_renamed_default_branch! 167 @commit ||= @repository.commit(@ref) 168 169 super 170 end 171 172 # rubocop: disable CodeReuse/ActiveRecord 173 def after_edit_path 174 from_merge_request = MergeRequestsFinder.new(current_user, project_id: @project.id).find_by(iid: params[:from_merge_request_iid]) 175 if from_merge_request && @branch_name == @ref 176 diffs_project_merge_request_path(from_merge_request.target_project, from_merge_request) + 177 "##{hexdigest(@path)}" 178 else 179 project_blob_path(@project, File.join(@branch_name, @path)) 180 end 181 end 182 # rubocop: enable CodeReuse/ActiveRecord 183 184 def after_delete_path 185 branch = BranchesFinder.new(@repository, search: @ref).execute.first 186 if @repository.tree(branch.target, tree_path).entries.empty? 187 project_tree_path(@project, @ref) 188 else 189 project_tree_path(@project, File.join(@ref, tree_path)) 190 end 191 end 192 193 def editor_variables 194 @branch_name = params[:branch_name] 195 196 @file_path = 197 if action_name.to_s == 'create' 198 if params[:file].present? 199 params[:file_name] = params[:file].original_filename 200 end 201 202 File.join(@path, params[:file_name]) 203 elsif params[:file_path].present? 204 params[:file_path] 205 else 206 @path 207 end 208 209 if params[:file].present? 210 params[:content] = params[:file] 211 end 212 213 @commit_params = { 214 file_path: @file_path, 215 commit_message: params[:commit_message], 216 previous_path: @path, 217 file_content: params[:content], 218 file_content_encoding: params[:encoding], 219 last_commit_sha: params[:last_commit_sha] 220 } 221 end 222 223 def validate_diff_params 224 return if params[:full] 225 226 if [:since, :to, :offset].any? { |key| params[key].blank? } 227 head :ok 228 end 229 end 230 231 def set_last_commit_sha 232 @last_commit_sha = Gitlab::Git::Commit 233 .last_for_path(@repository, @ref, @path, literal_pathspec: true).sha 234 end 235 236 def show_html 237 environment_params = @repository.branch_exists?(@ref) ? { ref: @ref } : { commit: @commit } 238 environment_params[:find_latest] = true 239 @environment = ::Environments::EnvironmentsByDeploymentsFinder.new(@project, current_user, environment_params).execute.last 240 @last_commit = @repository.last_commit_for_path(@commit.id, @blob.path, literal_pathspec: true) 241 @code_navigation_path = Gitlab::CodeNavigationPath.new(@project, @blob.commit_id).full_json_path_for(@blob.path) 242 243 render 'show' 244 end 245 246 def show_json 247 set_last_commit_sha 248 249 json = { 250 id: @blob.id, 251 last_commit_sha: @last_commit_sha, 252 path: blob.path, 253 name: blob.name, 254 extension: blob.extension, 255 size: blob.raw_size, 256 mime_type: blob.mime_type, 257 binary: blob.binary?, 258 simple_viewer: blob.simple_viewer&.class&.partial_name, 259 rich_viewer: blob.rich_viewer&.class&.partial_name, 260 show_viewer_switcher: !!blob.show_viewer_switcher?, 261 render_error: blob.simple_viewer&.render_error || blob.rich_viewer&.render_error, 262 raw_path: project_raw_path(project, @id), 263 blame_path: project_blame_path(project, @id), 264 commits_path: project_commits_path(project, @id), 265 tree_path: project_tree_path(project, File.join(@ref, tree_path)), 266 permalink: project_blob_path(project, File.join(@commit.id, @path)) 267 } 268 269 json.merge!(blob_json(@blob) || {}) unless params[:viewer] == 'none' 270 271 render json: json 272 end 273 274 def tree_path 275 @path.rpartition('/').first 276 end 277 278 def diff_params 279 params.permit(:full, :since, :to, :bottom, :unfold, :offset, :indent) 280 end 281 282 override :visitor_id 283 def visitor_id 284 current_user&.id 285 end 286 287 def create_success_path 288 if params[:code_quality_walkthrough] 289 project_pipelines_path(@project, code_quality_walkthrough: true) 290 else 291 project_blob_path(@project, File.join(@branch_name, @file_path)) 292 end 293 end 294 295 def track_experiment 296 return unless params[:code_quality_walkthrough] 297 298 experiment(:code_quality_walkthrough, namespace: @project.root_ancestor).track(:commit_created) 299 end 300end 301 302Projects::BlobController.prepend_mod 303