diff --git a/app/assets/javascripts/blob_edit/blob_edit_header.js b/app/assets/javascripts/blob_edit/blob_edit_header.js index dc76c639ecbe22f9f7e06efa32ddb900225840eb..dcf512b2bc4063cd1f913fa465e7861996d9108a 100644 --- a/app/assets/javascripts/blob_edit/blob_edit_header.js +++ b/app/assets/javascripts/blob_edit/blob_edit_header.js @@ -24,6 +24,9 @@ export default function initBlobEditHeader(editor) { projectId, projectPath, newMergeRequestPath, + targetProjectId, + targetProjectPath, + nextForkBranchName, } = el.dataset; return new Vue({ @@ -40,6 +43,9 @@ export default function initBlobEditHeader(editor) { projectId, projectPath, newMergeRequestPath, + targetProjectId, + targetProjectPath, + nextForkBranchName, emptyRepo: parseBoolean(emptyRepo), canPushCode: parseBoolean(canPushCode), canPushToBranch: parseBoolean(canPushToBranch), diff --git a/app/assets/javascripts/repository/pages/blob_edit_header.vue b/app/assets/javascripts/repository/pages/blob_edit_header.vue index 2ca6b412cd42a6b7b90c15870b0a21a3550caf43..d9f943ce74d59f466e7e558f0cc20d1c6ca16482 100644 --- a/app/assets/javascripts/repository/pages/blob_edit_header.vue +++ b/app/assets/javascripts/repository/pages/blob_edit_header.vue @@ -50,6 +50,9 @@ export default { 'projectId', 'projectPath', 'newMergeRequestPath', + 'targetProjectId', + 'targetProjectPath', + 'nextForkBranchName', ], data() { return { @@ -120,10 +123,11 @@ export default { this.originalFilePath = this.editor.getOriginalFilePath(); this.$refs[this.updateModalId].show(); }, - getUpdatePath(branch, filePath) { - const url = new URL(window.location.href); - url.pathname = joinPaths(this.projectPath, '-/blob', branch, filePath); - return url.toString(); + getUpdatePath(baseUrl, options) { + const { targetPath, branch, filePath } = options; + const urlObj = new URL(baseUrl instanceof URL ? baseUrl.toString() : baseUrl); + urlObj.pathname = joinPaths(targetPath, '-/blob', branch, filePath); + return urlObj.toString(); }, getResultingBranch(responseData, formData) { return responseData.branch || formData.branch_name || formData.original_branch; @@ -141,21 +145,21 @@ export default { }, editBlobWithUpdateFileApi(originalFormData) { const url = buildApiUrl(this.$options.UPDATE_FILE_PATH) - .replace(':id', this.projectId) + .replace(':id', this.targetProjectId) .replace(':file_path', encodeURIComponent(this.originalFilePath)); const data = { ...prepareDataForApiEdit(originalFormData), content: originalFormData.file, file_path: originalFormData.file_path, - id: this.projectId, + id: this.targetProjectId, last_commit_id: originalFormData.last_commit_sha, }; return axios.put(url, data); }, editBlobWithCommitsApi(originalFormData) { - const url = buildApiUrl(this.$options.COMMIT_FILE_PATH).replace(':id', this.projectId); + const url = buildApiUrl(this.$options.COMMIT_FILE_PATH).replace(':id', this.targetProjectId); const action = { action: 'move', @@ -190,13 +194,17 @@ export default { } }, async handleEditFormSubmit(formData) { - const originalFormData = prepareEditFormData(formData, { + let originalFormData = prepareEditFormData(formData, { fileContent: this.fileContent, filePath: this.filePath, lastCommitSha: this.lastCommitSha, fromMergeRequestIid: this.fromMergeRequestIid, }); + if (!this.canPushToBranch) { + originalFormData = { ...originalFormData, branch_name: this.nextForkBranchName }; + } + try { const response = this.glFeatures.blobEditRefactor ? await this.editBlob(originalFormData) @@ -253,38 +261,84 @@ export default { handleEditBlobSuccess(responseData, formData) { const resultingBranch = this.getResultingBranch(responseData, formData); const isNewBranch = this.originalBranch !== resultingBranch; + const urlString = new URL(window.location.href).toString(); - if (this.fromMergeRequestIid && resultingBranch === this.originalBranch) { - const url = new URL(window.location.href); - url.pathname = joinPaths(this.projectPath, '/-/merge_requests/', this.fromMergeRequestIid); - const cleanUrl = removeParams(['from_merge_request_iid'], url.toString()); - visitUrl(cleanUrl); - return; - } - - if (formData.create_merge_request && isNewBranch) { - const mrUrl = mergeUrlParams( - { [MR_SOURCE_BRANCH]: resultingBranch }, - this.newMergeRequestPath, - ); - visitUrl(mrUrl); + if (this.shouldRedirectToExistingMR(resultingBranch)) { + this.redirectToExistingMergeRequest(urlString); + } else if (!this.canPushToBranch) { + this.redirectToForkMergeRequest(urlString, resultingBranch); + } else if (this.shouldCreateNewMR(formData, isNewBranch)) { + this.redirectToCreateMergeRequest(resultingBranch); } else { - const successPath = this.getUpdatePath( + this.redirectToBlobWithAlert({ + urlString, resultingBranch, - responseData.file_path || formData.file_path, - ); - const createMergeRequestNotChosen = !formData.create_merge_request; + responseData, + formData, + isNewBranch, + }); + } + }, + shouldRedirectToExistingMR(resultingBranch) { + return ( + this.fromMergeRequestIid && + resultingBranch === this.originalBranch && + !this.branchAllowsCollaboration + ); + }, + shouldCreateNewMR(formData, isNewBranch) { + return formData.create_merge_request && isNewBranch; + }, + redirectToExistingMergeRequest(url) { + const urlCopy = new URL(url); + urlCopy.pathname = joinPaths( + this.projectPath, + '/-/merge_requests/', + this.fromMergeRequestIid, + ); + const cleanUrl = removeParams(['from_merge_request_iid'], urlCopy.toString()); + visitUrl(cleanUrl); + }, + redirectToCreateMergeRequest(resultingBranch) { + const mrUrl = mergeUrlParams( + { [MR_SOURCE_BRANCH]: resultingBranch }, + this.newMergeRequestPath, + ); + visitUrl(mrUrl); + }, + redirectToForkMergeRequest(url, resultingBranch) { + const urlCopy = new URL(url); + urlCopy.pathname = joinPaths(this.targetProjectPath, '/-/merge_requests/new'); - const message = this.successMessageForAlert(isNewBranch, createMergeRequestNotChosen); + const mrParams = { + 'merge_request[source_project_id]': this.targetProjectId, + 'merge_request[source_branch]': resultingBranch, + 'merge_request[target_project_id]': this.projectId, + 'merge_request[target_branch]': this.originalBranch, + }; - saveAlertToLocalStorage({ - message, - messageLinks: { changesLink: successPath }, - variant: VARIANT_INFO, - }); + const finalMrUrl = mergeUrlParams(mrParams, urlCopy.toString()); + visitUrl(finalMrUrl); + }, + redirectToBlobWithAlert(options) { + const { url, resultingBranch, responseData, formData, isNewBranch } = options; + const cleanUrl = removeParams(['from_merge_request_iid'], url); + const successPath = this.getUpdatePath(cleanUrl, { + targetPath: this.targetProjectPath, + branch: resultingBranch, + filePath: responseData.file_path || formData.file_path, + }); + const createMergeRequestNotChosen = !formData.create_merge_request; - visitUrl(successPath); - } + const message = this.successMessageForAlert(isNewBranch, createMergeRequestNotChosen); + + saveAlertToLocalStorage({ + message, + messageLinks: { changesLink: successPath }, + variant: VARIANT_INFO, + }); + + visitUrl(successPath); }, handleControllerSuccess(responseData) { if (responseData.filePath) { diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb index 92e7443c8cc33427ef8a673e105f452f5467b5f7..bf8ec13789ce4e223817c44b020bbbf0dc1ab380 100644 --- a/app/controllers/projects/blob_controller.rb +++ b/app/controllers/projects/blob_controller.rb @@ -259,6 +259,13 @@ def editor_variables params[:content] = params[:file] if params[:file].present? + # Determine fork project ID if user is working on a fork + @fork_project_id = if can_collaborate_with_project?(project, ref: @ref) + nil + else + current_user.fork_of(project)&.id + end + @commit_params = { file_path: @file_path, commit_message: params[:commit_message], diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb index b7e0f568f5efc4310856630ff1017132c709757a..44d4b1bb2350ee25e9ad08a400eb81e10760fbc9 100644 --- a/app/helpers/blob_helper.rb +++ b/app/helpers/blob_helper.rb @@ -328,38 +328,80 @@ def vue_blob_header_app_data(project, blob, ref) def edit_blob_app_data(project, id, blob, ref, action) is_update = action == 'update' is_create = action == 'create' - update_path = if is_update - project_update_blob_path(project, id) - elsif is_create - project_create_blob_path(project, id) - end - - cancel_path = if is_update - project_blob_path(project, id) - elsif is_create - project_tree_path(project, id) - end + can_push_to_branch = project.present(current_user: current_user).can_current_user_push_to_branch?(ref) { action: action.to_s, - update_path: update_path, - cancel_path: cancel_path, + update_path: edit_blob_update_path(project, id, is_update, is_create), + cancel_path: edit_blob_cancel_path(project, id, is_update, is_create), original_branch: ref, target_branch: selected_branch, can_push_code: can?(current_user, :push_code, project).to_s, - can_push_to_branch: project.present(current_user: current_user).can_current_user_push_to_branch?(ref).to_s, + can_push_to_branch: can_push_to_branch.to_s, empty_repo: project.empty_repo?.to_s, blob_name: is_update ? blob.name : nil, branch_allows_collaboration: project.branch_allows_collaboration?(current_user, ref).to_s, last_commit_sha: @last_commit_sha, project_id: project.id, project_path: project.full_path, - new_merge_request_path: project_new_merge_request_path(project) + new_merge_request_path: project_new_merge_request_path(project), + target_project_id: edit_blob_target_project_id(project, ref, can_push_to_branch), + target_project_path: edit_blob_target_project_path(project, can_push_to_branch), + next_fork_branch_name: edit_blob_fork_project(project)&.repository&.next_branch('patch') } end private + def edit_blob_target_project_id(project, ref, can_push_to_branch) + return project.id if can_push_to_branch + + edit_blob_fork_project_id(project, ref) || project.id + end + + def edit_blob_target_project_path(project, can_push_to_branch) + return project.full_path if can_push_to_branch + + edit_blob_fork_project_path(project) || project.full_path + end + + def edit_blob_update_path(project, id, is_update, is_create) + if is_update + project_update_blob_path(project, id) + elsif is_create + project_create_blob_path(project, id) + end + end + + def edit_blob_cancel_path(project, id, is_update, is_create) + if is_update + project_blob_path(project, id) + elsif is_create + project_tree_path(project, id) + end + end + + def edit_blob_fork_project_id(project, ref) + return unless can_collaborate_with_project?(project, ref: ref) + + current_user&.fork_of(project)&.id + end + + def edit_blob_fork_project_path(project) + return unless current_user + + fork_of_project = current_user.fork_of(project) + return unless current_user.already_forked?(project) && !current_user.has_groups_allowing_project_creation? + + namespace_project_path(current_user, fork_of_project) + end + + def edit_blob_fork_project(project) + return unless current_user + + current_user&.fork_of(project) + end + def archive_download_links(project, ref, archive_prefix) Gitlab::Workhorse::ARCHIVE_FORMATS.map do |fmt| { diff --git a/lib/api/files.rb b/lib/api/files.rb index 8d0db574770273064bdd135c04983c6561834961..e26eea2a4b83143563e357dd527b9cd1b0e9c29c 100644 --- a/lib/api/files.rb +++ b/lib/api/files.rb @@ -66,6 +66,18 @@ def authorize_ai_access! forbidden!('Insufficient permissions for Duo Agent Platform') end + def user_access + @user_access ||= Gitlab::UserAccess.new(current_user, container: user_project) + end + + def authorize_push_to_branch!(branch) + authenticate! + + return if user_access.can_push_to_branch?(branch) + + forbidden!("You are not allowed to push into this branch") + end + def commit_response(attrs) { file_path: attrs[:file_path], @@ -281,7 +293,7 @@ def filter_file_params!(params) end post ':id/repository/files/:file_path/authorize' do workhorse_authorize_commits_body_upload! do - authorize! :push_code, user_project + authorize_push_to_branch!(params[:branch]) end end @@ -293,10 +305,10 @@ def filter_file_params!(params) end post ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS, urgency: :low do require_gitlab_workhorse! - authorize! :push_code, user_project file_params = validate_file_params!(file_params_from_body_upload) file_params[:file_path] = params[:file_path] + authorize_push_to_branch!(file_params[:branch]) result = ::Files::CreateService.new(user_project, current_user, commit_params(file_params)).execute @@ -321,7 +333,7 @@ def filter_file_params!(params) end put ':id/repository/files/:file_path/authorize' do workhorse_authorize_commits_body_upload! do - authorize! :push_code, user_project + authorize_push_to_branch!(params[:branch]) end end @@ -333,10 +345,10 @@ def filter_file_params!(params) end put ":id/repository/files/:file_path", requirements: FILE_ENDPOINT_REQUIREMENTS, urgency: :low do require_gitlab_workhorse! - authorize! :push_code, user_project file_params = validate_file_params!(file_params_from_body_upload) file_params[:file_path] = params[:file_path] + authorize_push_to_branch!(params[:branch]) begin result = ::Files::UpdateService.new(user_project, current_user, commit_params(file_params)).execute diff --git a/spec/features/merge_request/maintainer_edits_fork_spec.rb b/spec/features/merge_request/maintainer_edits_fork_spec.rb index 92969a89c067a2fbeb48008a49fb802a6a88a8af..f50af8dace9da4a3880786524140df6e0d6cd8af 100644 --- a/spec/features/merge_request/maintainer_edits_fork_spec.rb +++ b/spec/features/merge_request/maintainer_edits_fork_spec.rb @@ -24,7 +24,6 @@ end before do - stub_feature_flags(blob_edit_refactor: false) target_project.add_maintainer(user) sign_in(user) diff --git a/spec/features/projects/files/editing_a_file_spec.rb b/spec/features/projects/files/editing_a_file_spec.rb index 798155aa1d248a8990d5b1d6877e9ac63b418953..416ae48b4d3b6b93f251ac0f330c225253b7bafa 100644 --- a/spec/features/projects/files/editing_a_file_spec.rb +++ b/spec/features/projects/files/editing_a_file_spec.rb @@ -91,7 +91,7 @@ end expect(page).to have_content( - 'An error occurred editing the blob' + 'You are attempting to update a file that has changed since you started editing it.' ) end end diff --git a/spec/features/projects/files/user_edits_files_spec.rb b/spec/features/projects/files/user_edits_files_spec.rb index 987083fef361b9f1a42f8c50398e8916e0826d08..b8adc23cbd79a4317021da98517904660b0039d3 100644 --- a/spec/features/projects/files/user_edits_files_spec.rb +++ b/spec/features/projects/files/user_edits_files_spec.rb @@ -184,7 +184,6 @@ context 'when an user does not have write access', :js do before do - stub_feature_flags(blob_edit_refactor: false) project2.add_reporter(user) visit(project2_tree_path_root_ref) wait_for_requests @@ -278,13 +277,37 @@ def expect_fork_status wait_for_requests expect(page).to have_content('New commit message') + expect(page).to have_css 'div.branch-selector', text: 'patch-1' + end + + it 'commits a renamed file in a forked project', :sidekiq_might_not_need_inline do + click_link('.gitignore') + edit_in_single_file_editor + + expect_fork_prompt + click_link_or_button('Fork') + + edit_in_single_file_editor + + fill_in _('File path'), with: '.gitignore-example' + + click_button('Commit changes') + + within_testid('commit-change-modal') do + fill_in(:commit_message, with: 'New commit message', visible: true) + click_button('Commit changes') + end + + wait_for_requests + + expect(page).to have_content('New commit message') + expect(page).to have_css 'div.branch-selector', text: 'patch-1' end context 'when the user already had a fork of the project', :js do let!(:forked_project) { fork_project(project2, user, namespace: user.namespace, repository: true) } before do - stub_feature_flags(blob_edit_refactor: false) visit(project2_tree_path_root_ref) wait_for_requests end diff --git a/spec/frontend/repository/pages/blob_edit_header_spec.js b/spec/frontend/repository/pages/blob_edit_header_spec.js index 4e034c8d62e2db89c4cbee92069552f0e7f740ca..8d157dc442ca00cecaa2287d50007d2f3f7bafd7 100644 --- a/spec/frontend/repository/pages/blob_edit_header_spec.js +++ b/spec/frontend/repository/pages/blob_edit_header_spec.js @@ -56,6 +56,9 @@ describe('BlobEditHeader', () => { projectId: 123, projectPath: 'gitlab-org/gitlab', newMergeRequestPath: 'merge_request/new/123', + targetProjectId: 123, + targetProjectPath: 'gitlab-org/gitlab', + nextForkBranchName: null, ...provided, glFeatures: { blobEditRefactor: true, ...glFeatures }, }, @@ -230,34 +233,46 @@ describe('BlobEditHeader', () => { removeParamsSpy.mockRestore(); }); - it('on submit to new branch to a fork repo, saves success message with "original project" text', async () => { - // Create wrapper with canPushToBranch: false to simulate fork scenario + it('when branchAllowsCollaboration is true, skips MR redirect and shows success message', async () => { + // Mock URL with from_merge_request_iid parameter + const originalLocation = window.location; + delete window.location; + window.location = new URL( + 'http://test.host/gitlab-org/gitlab/-/edit/main/test.js?from_merge_request_iid=19', + ); + createWrapper({ glFeatures: { blobEditRefactor: true }, - provided: { canPushToBranch: false }, + provided: { + branchAllowsCollaboration: true, + }, }); - // Need to click the commit button first to open modal and set up file content clickCommitChangesButton(); - mock.onPut().replyOnce(HTTP_STATUS_OK, { - branch: 'feature', + branch: 'main', // Same as originalBranch file_path: 'test.js', }); + await submitForm(); + await axios.waitForAll(); + // Should NOT redirect to MR, but instead show success message and redirect to blob expect(saveAlertToLocalStorage).toHaveBeenCalledWith({ message: - 'Your %{changesLinkStart}changes%{changesLinkEnd} have been committed successfully. You can now submit a merge request to get this change into the original project.', + 'Your %{changesLinkStart}changes%{changesLinkEnd} have been committed successfully.', messageLinks: { - changesLink: 'http://test.host/gitlab-org/gitlab/-/blob/feature/test.js', + changesLink: 'http://test.host/gitlab-org/gitlab/-/blob/main/test.js', }, variant: 'info', }); expect(visitUrlSpy).toHaveBeenCalledWith( - 'http://test.host/gitlab-org/gitlab/-/blob/feature/test.js', + 'http://test.host/gitlab-org/gitlab/-/blob/main/test.js', ); + + // Restore original location + window.location = originalLocation; }); describe('error handling', () => { @@ -326,6 +341,11 @@ describe('BlobEditHeader', () => { clickCommitChangesButton(); }); + afterEach(() => { + // Restore to initial value + mockEditor.filepathFormMediator.$filenameInput.val.mockReturnValue('test.js'); + }); + it('uses commits API when file path changes', async () => { mock.onPost().replyOnce(HTTP_STATUS_OK, {}); @@ -394,6 +414,75 @@ describe('BlobEditHeader', () => { expect(visitUrlSpy).not.toHaveBeenCalled(); }); }); + + describe('when working on a fork', () => { + beforeEach(async () => { + createWrapper({ + glFeatures: { blobEditRefactor: true }, + provided: { + canPushToBranch: false, + targetProjectId: 456, + targetProjectPath: 'user/gitlab-fork', + nextForkBranchName: 'patch-1', + }, + }); + + clickCommitChangesButton(); + mock.onPut().replyOnce(HTTP_STATUS_OK, { + branch: 'patch-1', + file_path: 'test.js', + }); + + await submitForm(); + }); + + it('on submit, redirects to merge request creation', () => { + expect(mock.history.put).toHaveLength(1); + expect(mock.history.put[0].url).toBe('/api/v4/projects/456/repository/files/test.js'); + const putData = JSON.parse(mock.history.put[0].data); + expect(putData.content).toBe(content); + + // this should actually redirect to source project + expect(visitUrlSpy).toHaveBeenCalledWith( + 'http://test.host/user/gitlab-fork/-/merge_requests/new?merge_request%5Bsource_project_id%5D=456&merge_request%5Bsource_branch%5D=patch-1&merge_request%5Btarget_project_id%5D=123&merge_request%5Btarget_branch%5D=main', + ); + expect(saveAlertToLocalStorage).not.toHaveBeenCalled(); + }); + }); + + describe('when renaming a file in fork', () => { + beforeEach(async () => { + createWrapper({ + glFeatures: { blobEditRefactor: true }, + provided: { + canPushToBranch: false, + targetProjectId: 456, + targetProjectPath: 'user/gitlab-fork', + nextForkBranchName: 'patch-1', + }, + }); + mockEditor.filepathFormMediator.$filenameInput.val.mockReturnValue('renamed_test.js'); + mockEditor.getOriginalFilePath.mockReturnValue('test.js'); + clickCommitChangesButton(); + mock.onPost().replyOnce(HTTP_STATUS_OK, {}); + + await submitForm(); + }); + + afterEach(() => { + // Restore to initial value + mockEditor.filepathFormMediator.$filenameInput.val.mockReturnValue('test.js'); + }); + + it('redirects to merge request creation page after renaming file', () => { + expect(mock.history.post).toHaveLength(1); + expect(mock.history.post[0].url).toBe('/api/v4/projects/456/repository/commits'); + expect(visitUrlSpy).toHaveBeenCalledWith( + 'http://test.host/user/gitlab-fork/-/merge_requests/new?merge_request%5Bsource_project_id%5D=456&merge_request%5Bsource_branch%5D=patch-1&merge_request%5Btarget_project_id%5D=123&merge_request%5Btarget_branch%5D=main', + ); + expect(saveAlertToLocalStorage).not.toHaveBeenCalled(); + }); + }); }); describe('when blobEditRefactor is disabled', () => { diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb index cc3dc33d5c1afc91abfb4779213cf3407dfdbb78..9d1623df2b00886473137918eb98a827a274491d 100644 --- a/spec/helpers/blob_helper_spec.rb +++ b/spec/helpers/blob_helper_spec.rb @@ -480,6 +480,7 @@ before do allow(helper).to receive(:current_user).and_return(user) allow(helper).to receive(:selected_branch).and_return(ref) + allow(user).to receive(:namespace).and_return(build_stubbed(:namespace, owner: user)) end context 'when editing a blob' do @@ -487,6 +488,7 @@ project_presenter = instance_double(ProjectPresenter) allow(helper).to receive(:can?).with(user, :push_code, project).and_return(true) + allow(helper).to receive(:can?).with(user, :create_merge_request_in, project).and_return(true) allow(project).to receive(:present).and_return(project_presenter) allow(project_presenter).to receive(:can_current_user_push_to_branch?).with(ref).and_return(true) allow(project).to receive(:empty_repo?).and_return(false) @@ -515,6 +517,40 @@ }) end + it 'returns data related to update action in a forked project' do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + allow(blob).to receive(:stored_externally?).and_return(false) + allow(project).to receive(:branch_allows_collaboration?).with(user, ref).and_return(false) + assign(:last_commit_sha, '782426692977b2cedb4452ee6501a404410f9b00') + # User cannot push to the original project's branch + project_presenter = instance_double(ProjectPresenter) + allow(project).to receive(:present).and_return(project_presenter) + allow(project_presenter).to receive(:can_current_user_push_to_branch?).with(ref).and_return(false) + + app_data = helper.edit_blob_app_data(project, id, blob, ref, "update") + + expect(app_data).to include({ + action: 'update', + update_path: project_update_blob_path(project, id), + cancel_path: project_blob_path(project, id), + original_branch: ref, + target_branch: ref, + can_push_code: 'true', + can_push_to_branch: 'false', + empty_repo: 'false', + blob_name: blob.name, + branch_allows_collaboration: 'false', + last_commit_sha: '782426692977b2cedb4452ee6501a404410f9b00', + project_id: project.id, + project_path: project.full_path, + new_merge_request_path: project_new_merge_request_path(project), + target_project_id: 999, + target_project_path: namespace_project_path(user, fork_project), + next_fork_branch_name: 'patch-1' + }) + end + it 'returns data related to create action' do expect(helper.edit_blob_app_data(project, id, blob, ref, "create")).to include({ action: 'create', @@ -535,8 +571,8 @@ context 'when user cannot push code' do it 'returns false for push permissions' do + allow(helper).to receive(:can?).with(user, :create_merge_request_in, project).and_return(false) allow(helper).to receive(:can?).with(user, :push_code, project).and_return(false) - expect(helper.edit_blob_app_data(project, id, blob, ref, "update")).to include( can_push_code: 'false' ) @@ -546,7 +582,8 @@ context 'when user cannot push to branch' do it 'returns false for branch push permissions' do project_presenter = instance_double(ProjectPresenter) - + allow(helper).to receive(:can?).with(user, :create_merge_request_in, project).and_return(false) + allow(helper).to receive(:can?).with(user, :push_code, project).and_return(false) allow(project).to receive(:present).and_return(project_presenter) allow(project_presenter).to receive(:can_current_user_push_to_branch?).with(ref).and_return(false) @@ -575,6 +612,169 @@ ) end end + + describe '#edit_blob_update_path' do + it 'returns update path for update action' do + path = helper.send(:edit_blob_update_path, project, id, true, false) + + expect(path).to eq(project_update_blob_path(project, id)) + end + + it 'returns create path for create action' do + path = helper.send(:edit_blob_update_path, project, id, false, true) + + expect(path).to eq(project_create_blob_path(project, id)) + end + end + + describe '#edit_blob_cancel_path' do + it 'returns blob path for update action' do + path = helper.send(:edit_blob_cancel_path, project, id, true, false) + + expect(path).to eq(project_blob_path(project, id)) + end + + it 'returns tree path for create action' do + path = helper.send(:edit_blob_cancel_path, project, id, false, true) + + expect(path).to eq(project_tree_path(project, id)) + end + end + + describe '#edit_blob_fork_project_id' do + context 'when user can collaborate with project' do + before do + allow(helper).to receive(:can_collaborate_with_project?).with(project, ref: ref).and_return(true) + end + + it 'returns fork project id when user has forked the project' do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + + fork_id = helper.send(:edit_blob_fork_project_id, project, ref) + + expect(fork_id).to eq(999) + end + + it 'returns nil when user has not forked the project' do + allow(user).to receive(:fork_of).with(project).and_return(nil) + + fork_id = helper.send(:edit_blob_fork_project_id, project, ref) + + expect(fork_id).to be_nil + end + end + + context 'when user cannot collaborate with project' do + before do + allow(helper).to receive(:can_collaborate_with_project?).with(project, ref: ref).and_return(false) + end + + it 'returns nil' do + fork_id = helper.send(:edit_blob_fork_project_id, project, ref) + + expect(fork_id).to be_nil + end + end + end + + describe '#edit_blob_fork_project' do + context 'when user is logged in' do + context 'when user has forked the project' do + it 'returns the fork project' do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + + fork = helper.send(:edit_blob_fork_project, project) + + expect(fork).to eq(fork_project) + end + end + + context 'when user has not forked the project' do + it 'returns nil' do + allow(user).to receive(:fork_of).with(project).and_return(nil) + + fork = helper.send(:edit_blob_fork_project, project) + + expect(fork).to be_nil + end + end + end + + context 'when user is not logged in' do + before do + allow(helper).to receive(:current_user).and_return(nil) + end + + it 'returns nil' do + fork = helper.send(:edit_blob_fork_project, project) + + expect(fork).to be_nil + end + end + end + + describe '#edit_blob_fork_project_path' do + context 'when user is logged in' do + context 'when user has forked the project and cannot create groups' do + before do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + allow(user).to receive(:already_forked?).with(project).and_return(true) + allow(user).to receive(:has_groups_allowing_project_creation?).and_return(false) + end + + it 'returns the fork project path' do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + + path = helper.send(:edit_blob_fork_project_path, project) + + expect(path).to eq(namespace_project_path(user, fork_project)) + end + end + + context 'when user has not forked the project' do + before do + allow(user).to receive(:already_forked?).with(project).and_return(false) + end + + it 'returns nil' do + path = helper.send(:edit_blob_fork_project_path, project) + + expect(path).to be_nil + end + end + + context 'when user can create groups' do + before do + fork_project = build_stubbed(:project, id: 999) + allow(user).to receive(:fork_of).with(project).and_return(fork_project) + allow(user).to receive(:already_forked?).with(project).and_return(true) + allow(user).to receive(:has_groups_allowing_project_creation?).and_return(true) + end + + it 'returns nil' do + path = helper.send(:edit_blob_fork_project_path, project) + + expect(path).to be_nil + end + end + end + + context 'when user is not logged in' do + before do + allow(helper).to receive(:current_user).and_return(nil) + end + + it 'returns nil' do + path = helper.send(:edit_blob_fork_project_path, project) + + expect(path).to be_nil + end + end + end end describe "#copy_blob_source_button" do