diff options
25 files changed, 355 insertions, 129 deletions
diff --git a/app/models/concerns/protected_ref.rb b/app/models/concerns/protected_ref.rb index 454374121f3..94eef4ff7cd 100644 --- a/app/models/concerns/protected_ref.rb +++ b/app/models/concerns/protected_ref.rb @@ -31,7 +31,7 @@ module ProtectedRef end end - def protected_ref_accessible_to?(ref, user, action:, protected_refs: nil) + def protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) access_levels_for_ref(ref, action: action, protected_refs: protected_refs).any? do |access_level| access_level.check_access(user) end diff --git a/app/models/project.rb b/app/models/project.rb index 141f3761bfe..172980b0ad4 100644 --- a/app/models/project.rb +++ b/app/models/project.rb @@ -1047,13 +1047,6 @@ class Project < ActiveRecord::Base "#{web_url}.git" end - def user_can_push_to_empty_repo?(user) - return false unless empty_repo? - return false unless Ability.allowed?(user, :push_code, self) - - !ProtectedBranch.default_branch_protected? || team.max_member_access(user.id) > Gitlab::Access::DEVELOPER - end - def forked? return true if fork_network && fork_network.root_project != self @@ -2014,10 +2007,11 @@ class Project < ActiveRecord::Base def fetch_branch_allows_maintainer_push?(user, branch_name) check_access = -> do + next false if empty_repo? + merge_request = source_of_merge_requests.opened .where(allow_maintainer_to_push: true) .find_by(source_branch: branch_name) - merge_request&.can_be_merged_by?(user) end diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb index 609780c5587..cb361a66591 100644 --- a/app/models/protected_branch.rb +++ b/app/models/protected_branch.rb @@ -4,6 +4,15 @@ class ProtectedBranch < ActiveRecord::Base protected_ref_access_levels :merge, :push + def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil) + # Masters, owners and admins are allowed to create the default branch + if default_branch_protected? && project.empty_repo? + return true if user.admin? || project.team.max_member_access(user.id) > Gitlab::Access::DEVELOPER + end + + super + end + # Check if branch name is marked as protected in the system def self.protected?(project, ref_name) return true if project.empty_repo? && default_branch_protected? diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb index 63ead5538cb..ad655a7b3f4 100644 --- a/app/presenters/project_presenter.rb +++ b/app/presenters/project_presenter.rb @@ -4,6 +4,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated include GitlabRoutingHelper include StorageHelper include TreeHelper + include ChecksCollaboration include Gitlab::Utils::StrongMemoize presents :project @@ -170,9 +171,11 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def can_current_user_push_to_branch?(branch) - return false unless repository.branch_exists?(branch) + user_access(project).can_push_to_branch?(branch) + end - ::Gitlab::UserAccess.new(current_user, project: project).can_push_to_branch?(branch) + def can_current_user_push_to_default_branch? + can_current_user_push_to_branch?(default_branch) end def files_anchor_data @@ -200,7 +203,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def new_file_anchor_data - if current_user && can_current_user_push_code? + if current_user && can_current_user_push_to_default_branch? OpenStruct.new(enabled: false, label: _('New file'), link: project_new_blob_path(project, default_branch || 'master'), @@ -209,7 +212,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def readme_anchor_data - if current_user && can_current_user_push_code? && repository.readme.blank? + if current_user && can_current_user_push_to_default_branch? && repository.readme.blank? OpenStruct.new(enabled: false, label: _('Add Readme'), link: add_readme_path) @@ -221,7 +224,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def changelog_anchor_data - if current_user && can_current_user_push_code? && repository.changelog.blank? + if current_user && can_current_user_push_to_default_branch? && repository.changelog.blank? OpenStruct.new(enabled: false, label: _('Add Changelog'), link: add_changelog_path) @@ -233,7 +236,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def license_anchor_data - if current_user && can_current_user_push_code? && repository.license_blob.blank? + if current_user && can_current_user_push_to_default_branch? && repository.license_blob.blank? OpenStruct.new(enabled: false, label: _('Add License'), link: add_license_path) @@ -245,7 +248,7 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated end def contribution_guide_anchor_data - if current_user && can_current_user_push_code? && repository.contribution_guide.blank? + if current_user && can_current_user_push_to_default_branch? && repository.contribution_guide.blank? OpenStruct.new(enabled: false, label: _('Add Contribution guide'), link: add_contribution_guide_path) diff --git a/app/services/quick_actions/interpret_service.rb b/app/services/quick_actions/interpret_service.rb index 6804dff2a9b..0215994b1a7 100644 --- a/app/services/quick_actions/interpret_service.rb +++ b/app/services/quick_actions/interpret_service.rb @@ -270,6 +270,26 @@ module QuickActions end end + desc 'Copy labels and milestone from other issue or merge request' + explanation do |source_issuable| + "Copy labels and milestone from #{source_issuable.to_reference}." + end + params '#issue | !merge_request' + condition do + issuable.persisted? && + current_user.can?(:"update_#{issuable.to_ability_name}", issuable) + end + parse_params do |issuable_param| + extract_references(issuable_param, :issue).first || + extract_references(issuable_param, :merge_request).first + end + command :copy_metadata do |source_issuable| + if source_issuable.present? && source_issuable.project.id == issuable.project.id + @updates[:add_label_ids] = source_issuable.labels.map(&:id) + @updates[:milestone_id] = source_issuable.milestone.id if source_issuable.milestone + end + end + desc 'Add a todo' explanation 'Adds a todo.' condition do diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 2f69da593cd..a066f9f4cca 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -58,7 +58,9 @@ touch README.md git add README.md git commit -m "add README" - git push -u origin master + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin master %fieldset %h5 Existing folder @@ -69,7 +71,9 @@ git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} git add . git commit -m "Initial commit" - git push -u origin master + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin master %fieldset %h5 Existing Git repository @@ -78,8 +82,10 @@ cd existing_repo git remote rename origin old-origin git remote add origin #{ content_tag(:span, default_url_to_repo, class: 'clone')} - git push -u origin --all - git push -u origin --tags + - if @project.can_current_user_push_to_default_branch? + %span>< + git push -u origin --all + git push -u origin --tags - if can? current_user, :remove_project, @project .prepend-top-20 diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml index 2c80f7c3fa3..76f57320f99 100644 --- a/app/views/projects/registry/repositories/index.html.haml +++ b/app/views/projects/registry/repositories/index.html.haml @@ -29,7 +29,7 @@ docker login #{Gitlab.config.registry.host_port} %br %p - - deploy_token = link_to(_('deploy token'), help_page_path('user/projects/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') + - deploy_token = link_to(_('deploy token'), help_page_path('user/project/deploy_tokens/index', anchor: 'read-container-registry-images'), target: '_blank') = s_('ContainerRegistry|You can also %{deploy_token} for read-only access to the registry images.').html_safe % { deploy_token: deploy_token } %br %p diff --git a/changelogs/unreleased/add-copy-metadata-command.yml b/changelogs/unreleased/add-copy-metadata-command.yml new file mode 100644 index 00000000000..3bf25ae6ce0 --- /dev/null +++ b/changelogs/unreleased/add-copy-metadata-command.yml @@ -0,0 +1,5 @@ +--- +title: Add Copy metadata quick action +merge_request: 16473 +author: Mateusz Bajorski +type: added diff --git a/changelogs/unreleased/bvl-fix-maintainer-push-error.yml b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml new file mode 100644 index 00000000000..66ab8fbf884 --- /dev/null +++ b/changelogs/unreleased/bvl-fix-maintainer-push-error.yml @@ -0,0 +1,5 @@ +--- +title: Fix errors on pushing to an empty repository +merge_request: 18462 +author: +type: fixed diff --git a/doc/user/project/quick_actions.md b/doc/user/project/quick_actions.md index 442fc978284..2f4ed3493c2 100644 --- a/doc/user/project/quick_actions.md +++ b/doc/user/project/quick_actions.md @@ -38,6 +38,7 @@ do. | `/award :emoji:` | Toggle award for :emoji: | | `/board_move ~column` | Move issue to column on the board | | `/duplicate #issue` | Closes this issue and marks it as a duplicate of another issue | -| `/move path/to/project` | Moves issue to another project | -| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | -| `/shrug` | Append the comment with `¯\_(ツ)_/¯` |
\ No newline at end of file +| `/move path/to/project` | Moves issue to another project | +| `/tableflip` | Append the comment with `(╯°□°)╯︵ ┻━┻` | +| `/shrug` | Append the comment with `¯\_(ツ)_/¯` | +| <code>/copy_metadata #issue | !merge_request</code> | Copy labels and milestone from other issue or merge request | diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index 69952cbb47c..8cf5d636743 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -63,10 +63,12 @@ module Gitlab request_cache def can_push_to_branch?(ref) return false unless can_access_git? - return false unless user.can?(:push_code, project) || project.branch_allows_maintainer_push?(user, ref) + return false unless project + + return false if !user.can?(:push_code, project) && !project.branch_allows_maintainer_push?(user, ref) if protected?(ProtectedBranch, project, ref) - project.user_can_push_to_empty_repo?(user) || protected_branch_accessible_to?(ref, action: :push) + protected_branch_accessible_to?(ref, action: :push) else true end @@ -101,6 +103,7 @@ module Gitlab def protected_branch_accessible_to?(ref, action:) ProtectedBranch.protected_ref_accessible_to?( ref, user, + project: project, action: action, protected_refs: project.protected_branches) end @@ -108,6 +111,7 @@ module Gitlab def protected_tag_accessible_to?(ref, action:) ProtectedTag.protected_ref_accessible_to?( ref, user, + project: project, action: action, protected_refs: project.protected_tags) end diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb index 1ae6152a1f0..803498d3b19 100644 --- a/spec/factories/projects.rb +++ b/spec/factories/projects.rb @@ -151,6 +151,13 @@ FactoryBot.define do end end + trait :stubbed_repository do + after(:build) do |project| + allow(project).to receive(:empty_repo?).and_return(false) + allow(project.repository).to receive(:empty?).and_return(false) + end + end + trait :wiki_repo do after(:create) do |project| raise 'Failed to create wiki repository!' unless project.create_wiki diff --git a/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb new file mode 100644 index 00000000000..b7d063596c1 --- /dev/null +++ b/spec/features/projects/blobs/user_creates_new_blob_in_new_project_spec.rb @@ -0,0 +1,52 @@ +require 'spec_helper' + +feature 'User creates blob in new project', :js do + let(:user) { create(:user) } + let(:project) { create(:project, :empty_repo) } + + shared_examples 'creating a file' do + before do + sign_in(user) + visit project_path(project) + end + + it 'allows the user to add a new file' do + click_link 'New file' + + find('#editor') + execute_script('ace.edit("editor").setValue("Hello world")') + + fill_in(:file_name, with: 'dummy-file') + + click_button('Commit changes') + + expect(page).to have_content('The file has been successfully created') + end + end + + describe 'as a master' do + before do + project.add_master(user) + end + + it_behaves_like 'creating a file' + end + + describe 'as an admin' do + let(:user) { create(:user, :admin) } + + it_behaves_like 'creating a file' + end + + describe 'as a developer' do + before do + project.add_developer(user) + sign_in(user) + visit project_path(project) + end + + it 'does not allow pushing to the default branch' do + expect(page).not_to have_content('New file') + end + end +end diff --git a/spec/features/projects/user_views_empty_project_spec.rb b/spec/features/projects/user_views_empty_project_spec.rb new file mode 100644 index 00000000000..7b982301ffc --- /dev/null +++ b/spec/features/projects/user_views_empty_project_spec.rb @@ -0,0 +1,43 @@ +require 'spec_helper' + +describe 'User views an empty project' do + let(:project) { create(:project, :empty_repo) } + let(:user) { create(:user) } + + shared_examples 'allowing push to default branch' do + before do + sign_in(user) + visit project_path(project) + end + + it 'shows push-to-master instructions' do + expect(page).to have_content('git push -u origin master') + end + end + + describe 'as a master' do + before do + project.add_master(user) + end + + it_behaves_like 'allowing push to default branch' + end + + describe 'as an admin' do + let(:user) { create(:user, :admin) } + + it_behaves_like 'allowing push to default branch' + end + + describe 'as a developer' do + before do + project.add_developer(user) + sign_in(user) + visit project_path(project) + end + + it 'does not show push-to-master instructions' do + expect(page).not_to have_content('git push -u origin master') + end + end +end diff --git a/spec/javascripts/activities_spec.js b/spec/javascripts/activities_spec.js index 909a1bf76bc..5dbdcd24296 100644 --- a/spec/javascripts/activities_spec.js +++ b/spec/javascripts/activities_spec.js @@ -3,24 +3,30 @@ import $ from 'jquery'; import 'vendor/jquery.endless-scroll'; import Activities from '~/activities'; +import Pager from '~/pager'; -(() => { +describe('Activities', () => { window.gon || (window.gon = {}); const fixtureTemplate = 'static/event_filter.html.raw'; const filters = [ { id: 'all', - }, { + }, + { id: 'push', name: 'push events', - }, { + }, + { id: 'merged', name: 'merge events', - }, { + }, + { id: 'comments', - }, { + }, + { id: 'team', - }]; + }, + ]; function getEventName(index) { const filter = filters[index]; @@ -32,31 +38,34 @@ import Activities from '~/activities'; return `#${filter.id}_event_filter`; } - describe('Activities', () => { - beforeEach(() => { - loadFixtures(fixtureTemplate); - new Activities(); - }); - - for (let i = 0; i < filters.length; i += 1) { - ((i) => { - describe(`when selecting ${getEventName(i)}`, () => { - beforeEach(() => { - $(getSelector(i)).click(); - }); - - for (let x = 0; x < filters.length; x += 1) { - ((x) => { - const shouldHighlight = i === x; - const testName = shouldHighlight ? 'should highlight' : 'should not highlight'; - - it(`${testName} ${getEventName(x)}`, () => { - expect($(getSelector(x)).parent().hasClass('active')).toEqual(shouldHighlight); - }); - })(x); - } - }); - })(i); - } + beforeEach(() => { + loadFixtures(fixtureTemplate); + spyOn(Pager, 'init').and.stub(); + new Activities(); }); -})(); + + for (let i = 0; i < filters.length; i += 1) { + (i => { + describe(`when selecting ${getEventName(i)}`, () => { + beforeEach(() => { + $(getSelector(i)).click(); + }); + + for (let x = 0; x < filters.length; x += 1) { + (x => { + const shouldHighlight = i === x; + const testName = shouldHighlight ? 'should highlight' : 'should not highlight'; + + it(`${testName} ${getEventName(x)}`, () => { + expect( + $(getSelector(x)) + .parent() + .hasClass('active'), + ).toEqual(shouldHighlight); + }); + })(x); + } + }); + })(i); + } +}); diff --git a/spec/javascripts/commits_spec.js b/spec/javascripts/commits_spec.js index 977298b9221..60d100e8544 100644 --- a/spec/javascripts/commits_spec.js +++ b/spec/javascripts/commits_spec.js @@ -3,6 +3,7 @@ import 'vendor/jquery.endless-scroll'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import CommitsList from '~/commits'; +import Pager from '~/pager'; describe('Commits List', () => { let commitsList; @@ -14,6 +15,7 @@ describe('Commits List', () => { </form> <ol id="commits-list"></ol> `); + spyOn(Pager, 'init').and.stub(); commitsList = new CommitsList(25); }); @@ -68,9 +70,10 @@ describe('Commits List', () => { mock.restore(); }); - it('should save the last search string', (done) => { + it('should save the last search string', done => { commitsList.searchField.val('GitLab'); - commitsList.filterResults() + commitsList + .filterResults() .then(() => { expect(ajaxSpy).toHaveBeenCalled(); expect(commitsList.lastSearch).toEqual('GitLab'); @@ -80,8 +83,9 @@ describe('Commits List', () => { .catch(done.fail); }); - it('should not make ajax call if the input does not change', (done) => { - commitsList.filterResults() + it('should not make ajax call if the input does not change', done => { + commitsList + .filterResults() .then(() => { expect(ajaxSpy).not.toHaveBeenCalled(); expect(commitsList.lastSearch).toEqual(''); diff --git a/spec/javascripts/ide/stores/actions_spec.js b/spec/javascripts/ide/stores/actions_spec.js index 0d603fac463..b6eadf56f9d 100644 --- a/spec/javascripts/ide/stores/actions_spec.js +++ b/spec/javascripts/ide/stores/actions_spec.js @@ -1,4 +1,4 @@ -import actions, { stageAllChanges, unstageAllChanges } from '~/ide/stores/actions'; +import actions, { stageAllChanges, unstageAllChanges, toggleFileFinder } from '~/ide/stores/actions'; import store from '~/ide/stores'; import * as types from '~/ide/stores/mutation_types'; import router from '~/ide/ide_router'; @@ -343,7 +343,7 @@ describe('Multi-file store actions', () => { describe('toggleFileFinder', () => { it('commits TOGGLE_FILE_FINDER', done => { testAction( - actions.toggleFileFinder, + toggleFileFinder, true, null, [{ type: 'TOGGLE_FILE_FINDER', payload: true }], diff --git a/spec/javascripts/pager_spec.js b/spec/javascripts/pager_spec.js index 8e1e6af2286..04f2e7ef4f9 100644 --- a/spec/javascripts/pager_spec.js +++ b/spec/javascripts/pager_spec.js @@ -1,13 +1,25 @@ +import $ from 'jquery'; import MockAdapter from 'axios-mock-adapter'; import axios from '~/lib/utils/axios_utils'; import Pager from '~/pager'; describe('pager', () => { + let axiosMock; + + beforeEach(() => { + axiosMock = new MockAdapter(axios); + }); + + afterEach(() => { + axiosMock.restore(); + }); + describe('init', () => { const originalHref = window.location.href; beforeEach(() => { setFixtures('<div class="content_list"></div><div class="loading"></div>'); + spyOn($.fn, 'endlessScroll').and.stub(); }); afterEach(() => { @@ -46,33 +58,28 @@ describe('pager', () => { describe('getOld', () => { const urlRegex = /(.*)some_list(.*)$/; - let mock; function mockSuccess() { - mock.onGet(urlRegex).reply(200, { + axiosMock.onGet(urlRegex).reply(200, { count: 0, html: '', }); } function mockError() { - mock.onGet(urlRegex).networkError(); + axiosMock.onGet(urlRegex).networkError(); } beforeEach(() => { - setFixtures('<div class="content_list" data-href="/some_list"></div><div class="loading"></div>'); + setFixtures( + '<div class="content_list" data-href="/some_list"></div><div class="loading"></div>', + ); spyOn(axios, 'get').and.callThrough(); - mock = new MockAdapter(axios); - Pager.init(); }); - afterEach(() => { - mock.restore(); - }); - - it('shows loader while loading next page', (done) => { + it('shows loader while loading next page', done => { mockSuccess(); spyOn(Pager.loading, 'show'); @@ -85,7 +92,7 @@ describe('pager', () => { }); }); - it('hides loader on success', (done) => { + it('hides loader on success', done => { mockSuccess(); spyOn(Pager.loading, 'hide'); @@ -98,7 +105,7 @@ describe('pager', () => { }); }); - it('hides loader on error', (done) => { + it('hides loader on error', done => { mockError(); spyOn(Pager.loading, 'hide'); @@ -111,7 +118,7 @@ describe('pager', () => { }); }); - it('sends request to url with offset and limit params', (done) => { + it('sends request to url with offset and limit params', done => { Pager.offset = 100; Pager.limit = 20; Pager.getOld(); diff --git a/spec/lib/gitlab/ci/status/build/play_spec.rb b/spec/lib/gitlab/ci/status/build/play_spec.rb index f128c1d4ca4..e2bb378f663 100644 --- a/spec/lib/gitlab/ci/status/build/play_spec.rb +++ b/spec/lib/gitlab/ci/status/build/play_spec.rb @@ -2,8 +2,8 @@ require 'spec_helper' describe Gitlab::Ci::Status::Build::Play do let(:user) { create(:user) } - let(:project) { build.project } - let(:build) { create(:ci_build, :manual) } + let(:project) { create(:project, :stubbed_repository) } + let(:build) { create(:ci_build, :manual, project: project) } let(:status) { Gitlab::Ci::Status::Core.new(build, user) } subject { described_class.new(status) } @@ -46,6 +46,8 @@ describe Gitlab::Ci::Status::Build::Play do context 'when user can not push to the branch' do before do build.project.add_developer(user) + create(:protected_branch, :masters_can_push, + name: build.ref, project: project) end it { is_expected.not_to have_action } diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index 40c8286b1b9..97b6069f64d 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -32,6 +32,12 @@ describe Gitlab::UserAccess do let(:empty_project) { create(:project_empty_repo) } let(:project_access) { described_class.new(user, project: empty_project) } + it 'returns true for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?('master')).to be_truthy + end + it 'returns true if user is master' do empty_project.add_master(user) @@ -71,6 +77,12 @@ describe Gitlab::UserAccess do let(:branch) { create :protected_branch, project: project, name: "test" } let(:not_existing_branch) { create :protected_branch, :developers_can_merge, project: project } + it 'returns true for admins' do + user.update!(admin: true) + + expect(access.can_push_to_branch?(branch.name)).to be_truthy + end + it 'returns true if user is a master' do project.add_master(user) diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb index 56161bfcc28..25d6597084c 100644 --- a/spec/models/environment_spec.rb +++ b/spec/models/environment_spec.rb @@ -1,7 +1,7 @@ require 'spec_helper' describe Environment do - let(:project) { create(:project) } + let(:project) { create(:project, :stubbed_repository) } subject(:environment) { create(:environment, project: project) } it { is_expected.to belong_to(:project) } @@ -201,7 +201,7 @@ describe Environment do end describe '#stop_with_action!' do - let(:user) { create(:admin) } + let(:user) { create(:user) } subject { environment.stop_with_action!(user) } diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb index 648f8a7944d..127eb998abe 100644 --- a/spec/models/project_spec.rb +++ b/spec/models/project_spec.rb @@ -1492,52 +1492,6 @@ describe Project do end end - describe '#user_can_push_to_empty_repo?' do - let(:project) { create(:project) } - let(:user) { create(:user) } - - it 'returns false when default_branch_protection is in full protection and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_FULL) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - - it 'returns false when default_branch_protection only lets devs merge and user is dev' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_MERGE) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - - it 'returns true when default_branch_protection lets devs push and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_DEV_CAN_PUSH) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns true when default_branch_protection is unprotected and user is developer' do - project.add_developer(user) - stub_application_setting(default_branch_protection: Gitlab::Access::PROTECTION_NONE) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns true when user is master' do - project.add_master(user) - - expect(project.user_can_push_to_empty_repo?(user)).to be_truthy - end - - it 'returns false when the repo is not empty' do - project.add_master(user) - expect(project).to receive(:empty_repo?).and_return(false) - - expect(project.user_can_push_to_empty_repo?(user)).to be_falsey - end - end - describe '#container_registry_url' do let(:project) { create(:project) } diff --git a/spec/presenters/project_presenter_spec.rb b/spec/presenters/project_presenter_spec.rb index 0a130c59037..830d2ee3b20 100644 --- a/spec/presenters/project_presenter_spec.rb +++ b/spec/presenters/project_presenter_spec.rb @@ -208,6 +208,17 @@ describe ProjectPresenter do it 'returns nil if user cannot push' do expect(presenter.new_file_anchor_data).to be_nil end + + context 'when the project is empty' do + let(:project) { create(:project, :empty_repo) } + + # Since we protect the default branch for empty repos + it 'is empty for a developer' do + project.add_developer(user) + + expect(presenter.new_file_anchor_data).to be_nil + end + end end describe '#readme_anchor_data' do diff --git a/spec/services/ci/retry_pipeline_service_spec.rb b/spec/services/ci/retry_pipeline_service_spec.rb index 6ce75c65c8c..f1acfc48468 100644 --- a/spec/services/ci/retry_pipeline_service_spec.rb +++ b/spec/services/ci/retry_pipeline_service_spec.rb @@ -235,6 +235,8 @@ describe Ci::RetryPipelineService, '#execute' do context 'when user is not allowed to trigger manual action' do before do project.add_developer(user) + create(:protected_branch, :masters_can_push, + name: pipeline.ref, project: project) end context 'when there is a failed manual action present' do diff --git a/spec/services/quick_actions/interpret_service_spec.rb b/spec/services/quick_actions/interpret_service_spec.rb index f793f55e51b..bd835a1fca6 100644 --- a/spec/services/quick_actions/interpret_service_spec.rb +++ b/spec/services/quick_actions/interpret_service_spec.rb @@ -306,6 +306,23 @@ describe QuickActions::InterpretService do end end + shared_examples 'copy_metadata command' do + it 'fetches issue or merge request and copies labels and milestone if content contains /copy_metadata reference' do + source_issuable # populate the issue + todo_label # populate this label + inreview_label # populate this label + _, updates = service.execute(content, issuable) + + expect(updates[:add_label_ids]).to match_array([inreview_label.id, todo_label.id]) + + if source_issuable.milestone + expect(updates[:milestone_id]).to eq(source_issuable.milestone.id) + else + expect(updates).not_to have_key(:milestone_id) + end + end + end + shared_examples 'shrug command' do it 'appends ¯\_(ツ)_/¯ to the comment' do new_content, _ = service.execute(content, issuable) @@ -757,6 +774,65 @@ describe QuickActions::InterpretService do let(:issuable) { issue } end + context '/copy_metadata command' do + let(:todo_label) { create(:label, project: project, title: 'To Do') } + let(:inreview_label) { create(:label, project: project, title: 'In Review') } + + it_behaves_like 'empty command' do + let(:content) { '/copy_metadata' } + let(:issuable) { issue } + end + + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference}" } + let(:issuable) { issue } + end + + context 'when the parent issuable has a milestone' do + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [todo_label, inreview_label], milestone: milestone) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + end + + context 'when more than one issuable is passed' do + it_behaves_like 'copy_metadata command' do + let(:source_issuable) { create(:labeled_issue, project: project, labels: [inreview_label, todo_label]) } + let(:other_label) { create(:label, project: project, title: 'Other') } + let(:other_source_issuable) { create(:labeled_issue, project: project, labels: [other_label]) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference} #{other_source_issuable.to_reference}" } + let(:issuable) { issue } + end + end + + context 'cross project references' do + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :public) } + let(:source_issuable) { create(:labeled_issue, project: other_project, labels: [todo_label, inreview_label]) } + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:content) { "/copy_metadata imaginary#1234" } + let(:issuable) { issue } + end + + it_behaves_like 'empty command' do + let(:other_project) { create(:project, :private) } + let(:source_issuable) { create(:issue, project: other_project) } + + let(:content) { "/copy_metadata #{source_issuable.to_reference(project)}" } + let(:issuable) { issue } + end + end + end + context '/duplicate command' do it_behaves_like 'duplicate command' do let(:issue_duplicate) { create(:issue, project: project) } |