diff options
22 files changed, 223 insertions, 77 deletions
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb index 3ceba5de7d3..ccc1a972a7d 100644 --- a/app/controllers/projects_controller.rb +++ b/app/controllers/projects_controller.rb @@ -325,7 +325,11 @@ class ProjectsController < Projects::ApplicationController if can?(current_user, :download_code, @project) return render 'projects/no_repo' unless @project.repository_exists? - render 'projects/empty' if @project.empty_repo? + if @project.empty_repo? + record_experiment_user(:invite_members_empty_project_version_a) + + render 'projects/empty' + end else if can?(current_user, :read_wiki, @project) @wiki = @project.wiki diff --git a/app/views/projects/_invite_members.html.haml b/app/views/projects/_invite_members.html.haml new file mode 100644 index 00000000000..ef030cabc93 --- /dev/null +++ b/app/views/projects/_invite_members.html.haml @@ -0,0 +1,8 @@ +%h4.gl-mt-0.gl-mb-3{ data: { testid: 'invite-member-section', + track_label: 'invite_members_empty_project', + track_event: 'render' } } + = s_('InviteMember|Invite your team') +%p= s_('InviteMember|Add members to this project and start collaborating with your team.') += link_to s_('InviteMember|Invite members'), project_project_members_path(@project, sort: :access_level_desc), + class: 'gl-button btn btn-success gl-mb-8 gl-xs-w-full', + data: { track_event: 'click_button', track_label: 'invite_members_empty_project' } diff --git a/app/views/projects/empty.html.haml b/app/views/projects/empty.html.haml index 2e0bb3b8529..2936eff45df 100644 --- a/app/views/projects/empty.html.haml +++ b/app/views/projects/empty.html.haml @@ -6,6 +6,8 @@ = render "home_panel" += render "invite_members" if experiment_enabled?(:invite_members_empty_project_version_a) && can_import_members? + %h4.gl-mt-0.gl-mb-3 = _('The repository for this project is empty') diff --git a/doc/administration/packages/container_registry.md b/doc/administration/packages/container_registry.md index 4a4c08ebffd..633129e98bd 100644 --- a/doc/administration/packages/container_registry.md +++ b/doc/administration/packages/container_registry.md @@ -101,7 +101,7 @@ If `auth` is not set up, users can pull Docker images without authentication. There are two ways you can configure the Registry's external domain. Either: - [Use the existing GitLab domain](#configure-container-registry-under-an-existing-gitlab-domain). - The Registry listens on a port and reuses GitLab's TLS certificate. + The Registry listens on a port and reuses the TLS certificate from GitLab. - [Use a completely separate domain](#configure-container-registry-under-its-own-domain) with a new TLS certificate for that domain. @@ -612,8 +612,8 @@ You can use GitLab as an auth endpoint with an external container registry. gitlab_rails['registry_issuer'] = "omnibus-gitlab-issuer" ``` - `gitlab_rails['registry_enabled'] = true` is needed to enable GitLab's - Container Registry features and authentication endpoint. GitLab's bundled + `gitlab_rails['registry_enabled'] = true` is needed to enable GitLab + Container Registry features and authentication endpoint. The GitLab bundled Container Registry service does not start, even with this enabled. 1. A certificate-key pair is required for GitLab and the external container diff --git a/doc/api/graphql/reference/gitlab_schema.graphql b/doc/api/graphql/reference/gitlab_schema.graphql index f065d985f0a..754f4d7cca3 100644 --- a/doc/api/graphql/reference/gitlab_schema.graphql +++ b/doc/api/graphql/reference/gitlab_schema.graphql @@ -2120,6 +2120,11 @@ input BoardListCreateInput { clientMutationId: String """ + Global ID of an existing iteration + """ + iterationId: IterationID + + """ Global ID of an existing label """ labelId: LabelID diff --git a/doc/api/graphql/reference/gitlab_schema.json b/doc/api/graphql/reference/gitlab_schema.json index b5ff3a42707..f0a99a7e522 100644 --- a/doc/api/graphql/reference/gitlab_schema.json +++ b/doc/api/graphql/reference/gitlab_schema.json @@ -5611,6 +5611,16 @@ "defaultValue": null }, { + "name": "iterationId", + "description": "Global ID of an existing iteration", + "type": { + "kind": "SCALAR", + "name": "IterationID", + "ofType": null + }, + "defaultValue": null + }, + { "name": "assigneeId", "description": "Global ID of an existing user", "type": { diff --git a/doc/user/admin_area/settings/sign_up_restrictions.md b/doc/user/admin_area/settings/sign_up_restrictions.md index 3c8ffc8e8c4..213c8c4116a 100644 --- a/doc/user/admin_area/settings/sign_up_restrictions.md +++ b/doc/user/admin_area/settings/sign_up_restrictions.md @@ -51,10 +51,13 @@ To enforce confirmation of the email address used for new sign ups: > [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/4315) in GitLab 13.6. -When the number of users reaches the user cap, any user who is added or requests access must be +When the number of billable users reaches the user cap, any user who is added or requests access must be [approved](../approving_users.md#approving-a-user) by an administrator before they can start using their account. +If an administrator increases or removes the user cap, the users in pending approval state will be +automatically approved in a background job. + ## Soft email confirmation > - [Introduced](https://gitlab.com/gitlab-org/gitlab-foss/-/issues/47003) in GitLab 12.2. diff --git a/lib/api/boards_responses.rb b/lib/api/boards_responses.rb index b83fa32ff22..89355c84401 100644 --- a/lib/api/boards_responses.rb +++ b/lib/api/boards_responses.rb @@ -45,7 +45,7 @@ module API def create_list create_list_service = - ::Boards::Lists::CreateService.new(board_parent, current_user, create_list_params) + ::Boards::Lists::CreateService.new(board_parent, current_user, declared_params.compact.with_indifferent_access) response = create_list_service.execute(board) @@ -56,10 +56,6 @@ module API end end - def create_list_params - params.slice(:label_id) - end - def move_list(list) move_list_service = ::Boards::Lists::MoveService.new(board_parent, current_user, { position: params[:position].to_i }) diff --git a/lib/gitlab/checks/push_check.rb b/lib/gitlab/checks/push_check.rb index 0f0d8bb82ec..47aa25aae4c 100644 --- a/lib/gitlab/checks/push_check.rb +++ b/lib/gitlab/checks/push_check.rb @@ -14,17 +14,9 @@ module Gitlab private def can_push? - user_access_can_push? || + user_access.can_push_for_ref?(ref) || project.branch_allows_collaboration?(user_access.user, branch_name) end - - def user_access_can_push? - if Feature.enabled?(:deploy_keys_on_protected_branches, project) - user_access.can_push_to_branch?(ref) - else - user_access.can_do_action?(:push_code) - end - end end end end diff --git a/lib/gitlab/deploy_key_access.rb b/lib/gitlab/deploy_key_access.rb index 97fd6db407b..ca16582d2b4 100644 --- a/lib/gitlab/deploy_key_access.rb +++ b/lib/gitlab/deploy_key_access.rb @@ -8,6 +8,10 @@ module Gitlab @container = container end + def can_push_for_ref?(ref) + can_push_to_branch?(ref) + end + private attr_reader :deploy_key diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb index c9b7c4c949e..94523813662 100644 --- a/lib/gitlab/experimentation.rb +++ b/lib/gitlab/experimentation.rb @@ -84,6 +84,9 @@ module Gitlab }, trial_registration_with_social_signin: { tracking_category: 'Growth::Conversion::Experiment::TrialRegistrationWithSocialSigning' + }, + invite_members_empty_project_version_a: { + tracking_category: 'Growth::Expansion::Experiment::InviteMembersEmptyProjectVersionA' } }.freeze diff --git a/lib/gitlab/user_access.rb b/lib/gitlab/user_access.rb index eec89e1ab72..0af7ad6ec17 100644 --- a/lib/gitlab/user_access.rb +++ b/lib/gitlab/user_access.rb @@ -81,6 +81,10 @@ module Gitlab end end + def can_push_for_ref?(_) + can_do_action?(:push_code) + end + private def can_push? diff --git a/locale/gitlab.pot b/locale/gitlab.pot index c8907263de2..f0336bb3af6 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -15207,12 +15207,21 @@ msgstr "" msgid "InviteMembers|Invite team members" msgstr "" +msgid "InviteMember|Add members to this project and start collaborating with your team." +msgstr "" + msgid "InviteMember|Invite Members (optional)" msgstr "" msgid "InviteMember|Invite another member" msgstr "" +msgid "InviteMember|Invite members" +msgstr "" + +msgid "InviteMember|Invite your team" +msgstr "" + msgid "InviteMember|Invited users will be added with developer level permissions. %{linkStart}View the documentation%{linkEnd} to see how to change this later." msgstr "" diff --git a/spec/controllers/projects_controller_spec.rb b/spec/controllers/projects_controller_spec.rb index 96015f7e830..bd7ef3db8b6 100644 --- a/spec/controllers/projects_controller_spec.rb +++ b/spec/controllers/projects_controller_spec.rb @@ -6,15 +6,15 @@ RSpec.describe ProjectsController do include ExternalAuthorizationServiceHelpers include ProjectForksHelper - let(:project) { create(:project, service_desk_enabled: false) } - let(:public_project) { create(:project, :public) } - let(:user) { create(:user) } + let_it_be(:project, reload: true) { create(:project, service_desk_enabled: false) } + let_it_be(:public_project) { create(:project, :public) } + let_it_be(:user) { create(:user) } let(:jpg) { fixture_file_upload('spec/fixtures/rails_sample.jpg', 'image/jpg') } let(:txt) { fixture_file_upload('spec/fixtures/doc_sample.txt', 'text/plain') } describe 'GET new' do context 'with an authenticated user' do - let(:group) { create(:group) } + let_it_be(:group) { create(:group) } before do sign_in(user) @@ -68,7 +68,7 @@ RSpec.describe ProjectsController do include DesignManagementTestHelpers render_views - let(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } + let_it_be(:project) { create(:project, :public, issues_access_level: ProjectFeature::PRIVATE) } before do enable_design_management @@ -227,10 +227,12 @@ RSpec.describe ProjectsController do end context "project with empty repo" do - let(:empty_project) { create(:project_empty_repo, :public) } + let_it_be(:empty_project) { create(:project_empty_repo, :public) } before do sign_in(user) + + allow(controller).to receive(:record_experiment_user).with(:invite_members_empty_project_version_a) end User.project_views.keys.each do |project_view| @@ -241,15 +243,16 @@ RSpec.describe ProjectsController do get :show, params: { namespace_id: empty_project.namespace, id: empty_project } end - it "renders the empty project view" do + it "renders the empty project view and records the experiment user", :aggregate_failures do expect(response).to render_template('empty') + expect(controller).to have_received(:record_experiment_user).with(:invite_members_empty_project_version_a) end end end end context "project with broken repo" do - let(:empty_project) { create(:project_broken_repo, :public) } + let_it_be(:empty_project) { create(:project_broken_repo, :public) } before do sign_in(user) @@ -273,7 +276,7 @@ RSpec.describe ProjectsController do end context "rendering default project view" do - let(:public_project) { create(:project, :public, :repository) } + let_it_be(:public_project) { create(:project, :public, :repository) } render_views @@ -397,8 +400,8 @@ RSpec.describe ProjectsController do end describe 'POST #archive' do - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } before do sign_in(user) @@ -445,8 +448,8 @@ RSpec.describe ProjectsController do end describe 'POST #unarchive' do - let(:group) { create(:group) } - let(:project) { create(:project, :archived, group: group) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, :archived, group: group) } before do sign_in(user) @@ -493,8 +496,8 @@ RSpec.describe ProjectsController do end describe '#housekeeping' do - let(:group) { create(:group) } - let(:project) { create(:project, group: group) } + let_it_be(:group) { create(:group) } + let_it_be(:project) { create(:project, group: group) } let(:housekeeping) { Projects::HousekeepingService.new(project) } context 'when authenticated as owner' do @@ -666,13 +669,13 @@ RSpec.describe ProjectsController do end context 'hashed storage' do - let(:project) { create(:project, :repository) } + let_it_be(:project) { create(:project, :repository) } it_behaves_like 'updating a project' end context 'legacy storage' do - let(:project) { create(:project, :repository, :legacy_storage) } + let_it_be(:project) { create(:project, :repository, :legacy_storage) } it_behaves_like 'updating a project' end @@ -745,9 +748,9 @@ RSpec.describe ProjectsController do describe '#transfer', :enable_admin_mode do render_views - let(:project) { create(:project, :repository) } - let(:admin) { create(:admin) } - let(:new_namespace) { create(:namespace) } + let_it_be(:project, reload: true) { create(:project, :repository) } + let_it_be(:admin) { create(:admin) } + let_it_be(:new_namespace) { create(:namespace) } it 'updates namespace' do sign_in(admin) @@ -791,7 +794,7 @@ RSpec.describe ProjectsController do end describe "#destroy", :enable_admin_mode do - let(:admin) { create(:admin) } + let_it_be(:admin) { create(:admin) } it "redirects to the dashboard", :sidekiq_might_not_need_inline do controller.instance_variable_set(:@project, project) @@ -971,7 +974,7 @@ RSpec.describe ProjectsController do end describe "GET refs" do - let(:project) { create(:project, :public, :repository) } + let_it_be(:project) { create(:project, :public, :repository) } it 'gets a list of branches and tags' do get :refs, params: { namespace_id: project.namespace, id: project, sort: 'updated_desc' } @@ -1043,7 +1046,7 @@ RSpec.describe ProjectsController do end context 'state filter on references' do - let(:issue) { create(:issue, :closed, project: public_project) } + let_it_be(:issue) { create(:issue, :closed, project: public_project) } let(:merge_request) { create(:merge_request, :closed, target_project: public_project) } it 'renders JSON body with state filter for issues' do @@ -1366,7 +1369,7 @@ RSpec.describe ProjectsController do end context 'private project with token authentication' do - let(:private_project) { create(:project, :private) } + let_it_be(:private_project) { create(:project, :private) } it_behaves_like 'authenticates sessionless user', :show, :atom, ignore_incrementing: true do before do @@ -1378,7 +1381,7 @@ RSpec.describe ProjectsController do end context 'public project with token authentication' do - let(:public_project) { create(:project, :public) } + let_it_be(:public_project) { create(:project, :public) } it_behaves_like 'authenticates sessionless user', :show, :atom, public: true do before do diff --git a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb b/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb deleted file mode 100644 index 8d239cb2cbf..00000000000 --- a/spec/features/projects/show/developer_views_empty_project_instructions_spec.rb +++ /dev/null @@ -1,20 +0,0 @@ -# frozen_string_literal: true - -require 'spec_helper' - -RSpec.describe 'Projects > Show > Developer views empty project instructions' do - let(:project) { create(:project, :empty_repo) } - let(:developer) { create(:user) } - - before do - project.add_developer(developer) - - sign_in(developer) - end - - it 'displays "git clone" instructions' do - visit project_path(project) - - expect(page).to have_content("git clone") - end -end diff --git a/spec/features/projects/show/user_sees_git_instructions_spec.rb b/spec/features/projects/show/user_sees_git_instructions_spec.rb index 3b77fd7eebf..febdb70de86 100644 --- a/spec/features/projects/show/user_sees_git_instructions_spec.rb +++ b/spec/features/projects/show/user_sees_git_instructions_spec.rb @@ -122,13 +122,11 @@ RSpec.describe 'Projects > Show > User sees Git instructions' do context 'when project is not empty' do let_it_be(:project) { create(:project, :public, :repository) } - before do - visit(project_path(project)) - end - context 'when not signed in' do before do allow(Gitlab.config.gitlab).to receive(:host).and_return('www.example.com') + + visit(project_path(project)) end include_examples 'shows details of non empty project' diff --git a/spec/fixtures/api/schemas/list.json b/spec/fixtures/api/schemas/list.json index f894ff584c8..65e140f9e28 100644 --- a/spec/fixtures/api/schemas/list.json +++ b/spec/fixtures/api/schemas/list.json @@ -10,7 +10,7 @@ "id": { "type": "integer" }, "list_type": { "type": "string", - "enum": ["backlog", "label", "closed"] + "enum": ["backlog", "label", "iteration", "closed"] }, "label": { "type": ["object", "null"], diff --git a/spec/frontend_integration/ide/user_opens_file_spec.js b/spec/frontend_integration/ide/user_opens_file_spec.js index 7fa6dcecc9e..98a73c7a029 100644 --- a/spec/frontend_integration/ide/user_opens_file_spec.js +++ b/spec/frontend_integration/ide/user_opens_file_spec.js @@ -3,7 +3,9 @@ import { screen } from '@testing-library/dom'; import * as ideHelper from './helpers/ide_helper'; import startWebIDE from './helpers/start'; -describe('IDE: User opens a file in the Web IDE', () => { +// https://gitlab.com/gitlab-org/gitlab/-/issues/293654#note_466432769 +// eslint-disable-next-line jest/no-disabled-tests +describe.skip('IDE: User opens a file in the Web IDE', () => { useOverclockTimers(); let vm; diff --git a/spec/lib/gitlab/checks/push_check_spec.rb b/spec/lib/gitlab/checks/push_check_spec.rb index 3ec38ec83bb..262438256b4 100644 --- a/spec/lib/gitlab/checks/push_check_spec.rb +++ b/spec/lib/gitlab/checks/push_check_spec.rb @@ -12,11 +12,32 @@ RSpec.describe Gitlab::Checks::PushCheck do context 'when the user is not allowed to push to the repo' do it 'raises an error' do - expect(user_access).to receive(:can_push_to_branch?).and_return(false) + expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false) expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') end end + + context 'when using a DeployKeyAccess instance' do + let(:deploy_key) { create(:deploy_key) } + let(:user_access) { Gitlab::DeployKeyAccess.new(deploy_key, container: project) } + + context 'when the deploy key cannot push to the targetted branch' do + it 'raises an error' do + allow(user_access).to receive(:can_push_to_branch?).and_return(false) + + expect { subject.validate! }.to raise_error(Gitlab::GitAccess::ForbiddenError, 'You are not allowed to push code to this project.') + end + end + + context 'when the deploy key can push to the targetted branch' do + it 'is valid' do + allow(user_access).to receive(:can_push_to_branch?).and_return(true) + + expect { subject.validate! }.not_to raise_error + end + end + end end end diff --git a/spec/lib/gitlab/deploy_key_access_spec.rb b/spec/lib/gitlab/deploy_key_access_spec.rb index 7989b427a85..e186e993d8f 100644 --- a/spec/lib/gitlab/deploy_key_access_spec.rb +++ b/spec/lib/gitlab/deploy_key_access_spec.rb @@ -25,7 +25,7 @@ RSpec.describe Gitlab::DeployKeyAccess do end end - describe '#can_push_to_branch?' do + describe '#can_push_for_ref?' do context 'push to a protected branch of this project via a deploy key' do before do create(:protected_branch_push_access_level, protected_branch: protected_branch, deploy_key: deploy_key) @@ -33,7 +33,7 @@ RSpec.describe Gitlab::DeployKeyAccess do context 'when the project has active deploy key owned by this user' do it 'returns true' do - expect(access.can_push_to_branch?(protected_branch.name)).to be_truthy + expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy end end @@ -41,7 +41,7 @@ RSpec.describe Gitlab::DeployKeyAccess do let(:deploy_key) { create(:deploy_key, user: create(:user)) } it 'returns false' do - expect(access.can_push_to_branch?(protected_branch.name)).to be_falsey + expect(access.can_push_for_ref?(protected_branch.name)).to be_falsey end end @@ -49,15 +49,15 @@ RSpec.describe Gitlab::DeployKeyAccess do let(:another_branch) { create(:protected_branch, :no_one_can_push, name: 'another_branch', project: project) } it 'returns false when trying to push to that other branch' do - expect(access.can_push_to_branch?(another_branch.name)).to be_falsey + expect(access.can_push_for_ref?(another_branch.name)).to be_falsey end context 'and the deploy key added for the first protected branch is also added for this other branch' do it 'returns true for both protected branches' do create(:protected_branch_push_access_level, protected_branch: another_branch, deploy_key: deploy_key) - expect(access.can_push_to_branch?(protected_branch.name)).to be_truthy - expect(access.can_push_to_branch?(another_branch.name)).to be_truthy + expect(access.can_push_for_ref?(protected_branch.name)).to be_truthy + expect(access.can_push_for_ref?(another_branch.name)).to be_truthy end end end diff --git a/spec/lib/gitlab/user_access_spec.rb b/spec/lib/gitlab/user_access_spec.rb index d6b1e3b2d4b..748a8336a25 100644 --- a/spec/lib/gitlab/user_access_spec.rb +++ b/spec/lib/gitlab/user_access_spec.rb @@ -310,4 +310,24 @@ RSpec.describe Gitlab::UserAccess do end end end + + describe '#can_push_for_ref?' do + let(:ref) { 'test_ref' } + + context 'when user cannot push_code to a project repository (eg. as a guest)' do + it 'is false' do + project.add_user(user, :guest) + + expect(access.can_push_for_ref?(ref)).to be_falsey + end + end + + context 'when user can push_code to a project repository (eg. as a developer)' do + it 'is true' do + project.add_user(user, :developer) + + expect(access.can_push_for_ref?(ref)).to be_truthy + end + end + end end diff --git a/spec/views/projects/empty.html.haml_spec.rb b/spec/views/projects/empty.html.haml_spec.rb new file mode 100644 index 00000000000..de83722160e --- /dev/null +++ b/spec/views/projects/empty.html.haml_spec.rb @@ -0,0 +1,82 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe 'projects/empty' do + let_it_be(:user) { create(:user) } + let_it_be(:project) { ProjectPresenter.new(create(:project, :empty_repo), current_user: user) } + + before do + allow(view).to receive(:experiment_enabled?).and_return(true) + allow(view).to receive(:current_user).and_return(user) + assign(:project, project) + end + + context 'when user can push code on the project' do + before do + allow(view).to receive(:can?).with(user, :push_code, project).and_return(true) + end + + it 'displays "git clone" instructions' do + render + + expect(rendered).to have_content("git clone") + end + end + + context 'when user can not push code on the project' do + before do + allow(view).to receive(:can?).with(user, :push_code, project).and_return(false) + end + + it 'does not display "git clone" instructions' do + render + + expect(rendered).not_to have_content("git clone") + end + end + + describe 'invite_members_empty_project_version_a experiment' do + let(:can_import_members) { true } + + before do + allow(view).to receive(:can_import_members?).and_return(can_import_members) + end + + shared_examples_for 'no invite member info' do + it 'does not show invite member info' do + render + + expect(rendered).not_to have_content('Invite your team') + end + end + + context 'when experiment is enabled' do + it 'shows invite members info', :aggregate_failures do + render + + expect(rendered).to have_selector('[data-track-event=render]') + expect(rendered).to have_selector('[data-track-label=invite_members_empty_project]', count: 2) + expect(rendered).to have_content('Invite your team') + expect(rendered).to have_content('Add members to this project and start collaborating with your team.') + expect(rendered).to have_link('Invite members', href: project_project_members_path(project, sort: :access_level_desc)) + expect(rendered).to have_selector('[data-track-event=click_button]') + end + + context 'when user does not have permissions to invite members' do + let(:can_import_members) { false } + + it_behaves_like 'no invite member info' + end + end + + context 'when experiment is not enabled' do + before do + allow(view).to receive(:experiment_enabled?) + .with(:invite_members_empty_project_version_a).and_return(false) + end + + it_behaves_like 'no invite member info' + end + end +end |