diff --git a/app/assets/javascripts/repository/components/blob_button_group.vue b/app/assets/javascripts/repository/components/blob_button_group.vue index e2ba5cea6ccdc785c45fba6e2d99e636cba6c72a..de6156d48dc644662363e0ccdf064fb2bfca9456 100644 --- a/app/assets/javascripts/repository/components/blob_button_group.vue +++ b/app/assets/javascripts/repository/components/blob_button_group.vue @@ -53,6 +53,10 @@ export default { type: Boolean, required: true, }, + canPushToBranch: { + type: Boolean, + required: true, + }, emptyRepo: { type: Boolean, required: true, @@ -126,6 +130,7 @@ export default { :target-branch="targetBranch || ref" :original-branch="originalBranch || ref" :can-push-code="canPushCode" + :can-push-to-branch="canPushToBranch" :empty-repo="emptyRepo" /> </div> diff --git a/app/assets/javascripts/repository/components/blob_content_viewer.vue b/app/assets/javascripts/repository/components/blob_content_viewer.vue index 2cc5a8a79d23b86d9e901a93fc1176542e194a5c..cea95645fa4bf23581ef4101805fe10b3d573448 100644 --- a/app/assets/javascripts/repository/components/blob_content_viewer.vue +++ b/app/assets/javascripts/repository/components/blob_content_viewer.vue @@ -106,6 +106,7 @@ export default { ideForkAndEditPath: '', storedExternally: false, canModifyBlob: false, + canCurrentUserPushToBranch: false, rawPath: '', externalStorageUrl: '', replacePath: '', @@ -266,6 +267,7 @@ export default { :replace-path="blobInfo.replacePath" :delete-path="blobInfo.webPath" :can-push-code="project.userPermissions.pushCode" + :can-push-to-branch="blobInfo.canCurrentUserPushToBranch" :empty-repo="project.repository.empty" :project-path="projectPath" :is-locked="isLocked" diff --git a/app/assets/javascripts/repository/components/delete_blob_modal.vue b/app/assets/javascripts/repository/components/delete_blob_modal.vue index e15f9b01c627844415e240886e9d8690c2158241..0d3dc06c2c8ecf03a3d53e9bb2486993a1c59e56 100644 --- a/app/assets/javascripts/repository/components/delete_blob_modal.vue +++ b/app/assets/javascripts/repository/components/delete_blob_modal.vue @@ -71,6 +71,10 @@ export default { type: Boolean, required: true, }, + canPushToBranch: { + type: Boolean, + required: true, + }, emptyRepo: { type: Boolean, required: true, @@ -176,9 +180,12 @@ export default { </template> <template v-else> <input type="hidden" name="original_branch" :value="originalBranch" /> - <!-- Once "push to branch" permission is made available, will need to add to conditional - Follow-up issue: https://gitlab.com/gitlab-org/gitlab/-/issues/335462 --> - <input v-if="createNewMr" type="hidden" name="create_merge_request" value="1" /> + <input + v-if="createNewMr || !canPushToBranch" + type="hidden" + name="create_merge_request" + value="1" + /> <gl-form-group :label="$options.i18n.COMMIT_LABEL" label-for="commit_message" diff --git a/app/assets/javascripts/repository/queries/blob_info.query.graphql b/app/assets/javascripts/repository/queries/blob_info.query.graphql index cf3892802fdecf5d7a216b43e2a70bf351de9a74..539719175e35f43c4bd9aeec51b405d8403c48b0 100644 --- a/app/assets/javascripts/repository/queries/blob_info.query.graphql +++ b/app/assets/javascripts/repository/queries/blob_info.query.graphql @@ -28,6 +28,7 @@ query getBlobInfo($projectPath: ID!, $filePath: String!, $ref: String!) { forkAndEditPath ideForkAndEditPath canModifyBlob + canCurrentUserPushToBranch storedExternally rawPath replacePath diff --git a/app/graphql/types/repository/blob_type.rb b/app/graphql/types/repository/blob_type.rb index 104171e6772d3fe9ca3f703f4605dcd5dd8b3cc3..cd4993ea86ddd6bf980c7c6d9b9fc782b006d6e7 100644 --- a/app/graphql/types/repository/blob_type.rb +++ b/app/graphql/types/repository/blob_type.rb @@ -91,6 +91,9 @@ class BlobType < BaseObject calls_gitaly: true, description: 'Whether the current user can modify the blob.' + field :can_current_user_push_to_branch, GraphQL::Types::Boolean, null: true, method: :can_current_user_push_to_branch?, + description: 'Whether the current user can push to the branch.' + def raw_text_blob object.data unless object.binary? end diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb index 5835a77d0b90d975313abdb1c8552a7837e1b0c1..3555c6c3d0cbf80b2c55ce667a2d459a47bd6a34 100644 --- a/app/presenters/blob_presenter.rb +++ b/app/presenters/blob_presenter.rb @@ -78,6 +78,12 @@ def can_modify_blob? super(blob, project, blob.commit_id) end + def can_current_user_push_to_branch? + return false unless current_user && project.repository.branch_exists?(blob.commit_id) + + user_access(project).can_push_to_branch?(blob.commit_id) + end + def ide_edit_path super(project, blob.commit_id, blob.path) end diff --git a/doc/api/graphql/reference/index.md b/doc/api/graphql/reference/index.md index d49eee5d15c3491f002278eaa4989867571ab9fa..c4167e3ecd215eb87aad333f4d422e84d51026b0 100644 --- a/doc/api/graphql/reference/index.md +++ b/doc/api/graphql/reference/index.md @@ -14072,6 +14072,7 @@ Returns [`Tree`](#tree). | Name | Type | Description | | ---- | ---- | ----------- | +| <a id="repositoryblobcancurrentuserpushtobranch"></a>`canCurrentUserPushToBranch` | [`Boolean`](#boolean) | Whether the current user can push to the branch. | | <a id="repositoryblobcanmodifyblob"></a>`canModifyBlob` | [`Boolean`](#boolean) | Whether the current user can modify the blob. | | <a id="repositoryblobeditblobpath"></a>`editBlobPath` | [`String`](#string) | Web path to edit the blob in the old-style editor. | | <a id="repositoryblobexternalstorageurl"></a>`externalStorageUrl` | [`String`](#string) | Web path to download the raw blob via external storage, if enabled. | diff --git a/ee/spec/frontend/repository/components/blob_button_group_spec.js b/ee/spec/frontend/repository/components/blob_button_group_spec.js index 3db9e53c65949f4fef5998ad453a6ed994d79c05..8719c5316db565f104343dce2034e993dec4a3f4 100644 --- a/ee/spec/frontend/repository/components/blob_button_group_spec.js +++ b/ee/spec/frontend/repository/components/blob_button_group_spec.js @@ -6,6 +6,7 @@ const DEFAULT_PROPS = { name: 'some name', path: 'some/path', canPushCode: true, + canPushToBranch: true, replacePath: 'some/replace/path', deletePath: 'some/delete/path', emptyRepo: false, diff --git a/spec/frontend/repository/components/blob_button_group_spec.js b/spec/frontend/repository/components/blob_button_group_spec.js index f2a3354f204c6364427d5e236bd30eeca28be2ae..9f9d574a8ed3750fa25514f6fafe60dfe5f36045 100644 --- a/spec/frontend/repository/components/blob_button_group_spec.js +++ b/spec/frontend/repository/components/blob_button_group_spec.js @@ -9,6 +9,7 @@ const DEFAULT_PROPS = { name: 'some name', path: 'some/path', canPushCode: true, + canPushToBranch: true, replacePath: 'some/replace/path', deletePath: 'some/delete/path', emptyRepo: false, diff --git a/spec/frontend/repository/components/delete_blob_modal_spec.js b/spec/frontend/repository/components/delete_blob_modal_spec.js index 2c62868f3916f2aff85fcd825cc1e8fef8626fdb..785783b2e75886802865f2ebec2addca7a01c038 100644 --- a/spec/frontend/repository/components/delete_blob_modal_spec.js +++ b/spec/frontend/repository/components/delete_blob_modal_spec.js @@ -13,6 +13,7 @@ const initialProps = { targetBranch: 'some-target-branch', originalBranch: 'main', canPushCode: true, + canPushToBranch: true, emptyRepo: false, }; @@ -103,22 +104,25 @@ describe('DeleteBlobModal', () => { ); it.each` - input | value | emptyRepo | canPushCode | exist - ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} - ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true} - ${'_method'} | ${'delete'} | ${false} | ${true} | ${true} - ${'_method'} | ${'delete'} | ${true} | ${false} | ${true} - ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true} - ${'original_branch'} | ${undefined} | ${true} | ${true} | ${false} - ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} - ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true} - ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${false} + input | value | emptyRepo | canPushCode | canPushToBranch | exist + ${'authenticity_token'} | ${'mock-csrf-token'} | ${false} | ${true} | ${true} | ${true} + ${'authenticity_token'} | ${'mock-csrf-token'} | ${true} | ${false} | ${true} | ${true} + ${'_method'} | ${'delete'} | ${false} | ${true} | ${true} | ${true} + ${'_method'} | ${'delete'} | ${true} | ${false} | ${true} | ${true} + ${'original_branch'} | ${initialProps.originalBranch} | ${false} | ${true} | ${true} | ${true} + ${'original_branch'} | ${undefined} | ${true} | ${true} | ${true} | ${false} + ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${true} + ${'create_merge_request'} | ${'1'} | ${false} | ${true} | ${true} | ${true} + ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${false} | ${true} + ${'create_merge_request'} | ${'1'} | ${false} | ${false} | ${true} | ${true} + ${'create_merge_request'} | ${undefined} | ${true} | ${false} | ${true} | ${false} `( 'passes $input as a hidden input with the correct value', - ({ input, value, emptyRepo, canPushCode, exist }) => { + ({ input, value, emptyRepo, canPushCode, canPushToBranch, exist }) => { createComponent({ emptyRepo, canPushCode, + canPushToBranch, }); const inputMethod = findForm().find(`input[name="${input}"]`); diff --git a/spec/frontend/repository/mock_data.js b/spec/frontend/repository/mock_data.js index adf5991ac3c8ec8e42022de7f9572d10dbdbfc7d..99018de6efd85c19595de442ba0deff125f4303b 100644 --- a/spec/frontend/repository/mock_data.js +++ b/spec/frontend/repository/mock_data.js @@ -11,6 +11,7 @@ export const simpleViewerMock = { forkAndEditPath: 'some_file.js/fork/edit', ideForkAndEditPath: 'some_file.js/fork/ide', canModifyBlob: true, + canCurrentUserPushToBranch: true, storedExternally: false, rawPath: 'some_file.js', replacePath: 'some_file.js/replace', diff --git a/spec/graphql/types/repository/blob_type_spec.rb b/spec/graphql/types/repository/blob_type_spec.rb index 7f37237f355b2ee3cc25761b3b479ab561c4785e..968cb026401a1b8697046050ba52b673cf485fdc 100644 --- a/spec/graphql/types/repository/blob_type_spec.rb +++ b/spec/graphql/types/repository/blob_type_spec.rb @@ -28,6 +28,7 @@ :rich_viewer, :plain_data, :can_modify_blob, + :can_current_user_push_to_branch, :ide_edit_path, :external_storage_url, :fork_and_edit_path, diff --git a/spec/presenters/blob_presenter_spec.rb b/spec/presenters/blob_presenter_spec.rb index 28e18708eab1dc1ff22714a02389befe0c7fd967..cd38e74e0eaf339d71929056937f5d3437677af2 100644 --- a/spec/presenters/blob_presenter_spec.rb +++ b/spec/presenters/blob_presenter_spec.rb @@ -31,6 +31,28 @@ it { expect(presenter.replace_path).to eq("/#{project.full_path}/-/create/#{blob.commit_id}/#{blob.path}") } end + describe '#can_current_user_push_to_branch' do + let(:branch_exists) { true } + + before do + allow(project.repository).to receive(:branch_exists?).with(blob.commit_id).and_return(branch_exists) + end + + it { expect(presenter.can_current_user_push_to_branch?).to eq(true) } + + context 'current_user is nil' do + let(:user) { nil } + + it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + end + + context 'branch does not exist' do + let(:branch_exists) { false } + + it { expect(presenter.can_current_user_push_to_branch?).to eq(false) } + end + end + describe '#pipeline_editor_path' do context 'when blob is .gitlab-ci.yml' do before do