diff --git a/.gitlab/merge_request_templates/New End To End Test.md b/.gitlab/merge_request_templates/New End To End Test.md index 9bd7f11d4a5..c38c3ce340a 100644 --- a/.gitlab/merge_request_templates/New End To End Test.md +++ b/.gitlab/merge_request_templates/New End To End Test.md @@ -11,6 +11,7 @@ Please link to the respective test case in the testcases project - [ ] Follow the end-to-end tests [style guide](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/style_guide.html) and [best practices](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/best_practices.html). - [ ] Use the appropriate [RSpec metadata tag(s)](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/rspec_metadata_tests.html#rspec-metadata-for-end-to-end-tests). - [ ] Ensure that a created resource is removed after test execution. +- [ ] Ensure that no [transient bugs](https://about.gitlab.com/handbook/engineering/quality/issue-triage/#transient-bugs) are hidden accidentally due to the usage of `waits` and `reloads`. - [ ] Verify the tags to ensure it runs on the desired test environments. - [ ] If this MR has a dependency on another MR, such as a GitLab QA MR, specify the order in which the MRs should be merged. - [ ] (If applicable) Create a follow-up issue to document [the special setup](https://docs.gitlab.com/ee/development/testing_guide/end_to_end/running_tests_that_require_special_setup.html) necessary to run the test: ISSUE_LINK diff --git a/app/assets/javascripts/pages/projects/edit/index.js b/app/assets/javascripts/pages/projects/edit/index.js index 7eeb0c852e5..5f1d3edc3ba 100644 --- a/app/assets/javascripts/pages/projects/edit/index.js +++ b/app/assets/javascripts/pages/projects/edit/index.js @@ -10,6 +10,7 @@ import initProjectPermissionsSettings from '../shared/permissions'; import initProjectDeleteButton from '~/projects/project_delete_button'; import UserCallout from '~/user_callout'; import initServiceDesk from '~/projects/settings_service_desk'; +import mountSearchSettings from './mount_search_settings'; document.addEventListener('DOMContentLoaded', () => { initFilePickers(); @@ -30,4 +31,6 @@ document.addEventListener('DOMContentLoaded', () => { '.js-general-settings-form, .js-mr-settings-form, .js-mr-approvals-form', ), ); + + mountSearchSettings(); }); diff --git a/app/assets/javascripts/pages/projects/edit/mount_search_settings.js b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js new file mode 100644 index 00000000000..6c477dd7e80 --- /dev/null +++ b/app/assets/javascripts/pages/projects/edit/mount_search_settings.js @@ -0,0 +1,12 @@ +const mountSearchSettings = async () => { + const el = document.querySelector('.js-search-settings-app'); + + if (el) { + const { default: initSearch } = await import( + /* webpackChunkName: 'search_settings' */ '~/search_settings' + ); + initSearch({ el }); + } +}; + +export default mountSearchSettings; diff --git a/app/assets/javascripts/search_settings/components/search_settings.vue b/app/assets/javascripts/search_settings/components/search_settings.vue new file mode 100644 index 00000000000..820055dc656 --- /dev/null +++ b/app/assets/javascripts/search_settings/components/search_settings.vue @@ -0,0 +1,129 @@ + + diff --git a/app/assets/javascripts/search_settings/constants.js b/app/assets/javascripts/search_settings/constants.js new file mode 100644 index 00000000000..499e42854ed --- /dev/null +++ b/app/assets/javascripts/search_settings/constants.js @@ -0,0 +1,11 @@ +// We do not consider these nodes in the search index +export const EXCLUDED_NODES = ['OPTION']; + +// Used to hide the sections that do not match * the search term +export const HIDE_CLASS = 'gl-display-none'; + +// used to highlight the text that matches the * search term +export const HIGHLIGHT_CLASS = 'gl-bg-orange-50'; + +// How many seconds to wait until the user * stops typing +export const TYPING_DELAY = 400; diff --git a/app/assets/javascripts/search_settings/index.js b/app/assets/javascripts/search_settings/index.js new file mode 100644 index 00000000000..1fb1a378ffb --- /dev/null +++ b/app/assets/javascripts/search_settings/index.js @@ -0,0 +1,23 @@ +import Vue from 'vue'; +import $ from 'jquery'; +import { expandSection, closeSection } from '~/settings_panels'; +import SearchSettings from '~/search_settings/components/search_settings.vue'; + +const initSearch = ({ el }) => + new Vue({ + el, + render: (h) => + h(SearchSettings, { + ref: 'searchSettings', + props: { + searchRoot: document.querySelector('#content-body'), + sectionSelector: 'section.settings', + }, + on: { + collapse: (section) => closeSection($(section)), + expand: (section) => expandSection($(section)), + }, + }), + }); + +export default initSearch; diff --git a/app/assets/javascripts/settings_panels.js b/app/assets/javascripts/settings_panels.js index ac5a7cfc4ba..1f1f6e42576 100644 --- a/app/assets/javascripts/settings_panels.js +++ b/app/assets/javascripts/settings_panels.js @@ -1,7 +1,7 @@ import $ from 'jquery'; import { __ } from './locale'; -function expandSection($section) { +export function expandSection($section) { $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Collapse')); // eslint-disable-next-line @gitlab/no-global-event-off $section.find('.settings-content').off('scroll.expandSection').scrollTop(0); @@ -13,7 +13,7 @@ function expandSection($section) { } } -function closeSection($section) { +export function closeSection($section) { $section.find('.js-settings-toggle:not(.js-settings-toggle-trigger-only)').text(__('Expand')); $section.find('.settings-content').on('scroll.expandSection', () => expandSection($section)); $section.removeClass('expanded'); @@ -24,7 +24,7 @@ function closeSection($section) { } } -function toggleSection($section) { +export function toggleSection($section) { $section.removeClass('no-animate'); if ($section.hasClass('expanded')) { closeSection($section); diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb index 279d3faa110..8b9055ae289 100644 --- a/app/models/ci/build.rb +++ b/app/models/ci/build.rb @@ -27,7 +27,8 @@ module Ci upload_multiple_artifacts: -> (build) { build.publishes_artifacts_reports? }, refspecs: -> (build) { build.merge_request_ref? }, artifacts_exclude: -> (build) { build.supports_artifacts_exclude? }, - multi_build_steps: -> (build) { build.multi_build_steps? } + multi_build_steps: -> (build) { build.multi_build_steps? }, + return_exit_code: -> (build) { build.exit_codes_defined? } }.freeze DEFAULT_RETRIES = { @@ -1038,6 +1039,10 @@ module Ci end end + def exit_codes_defined? + options.dig(:allow_failure_criteria, :exit_codes).present? + end + protected def run_status_commit_hooks! diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml index 3d3b8c28a17..ae1396340f3 100644 --- a/app/views/admin/runners/index.html.haml +++ b/app/views/admin/runners/index.html.haml @@ -5,34 +5,34 @@ .col-sm-6 .bs-callout %p - = (_"A 'Runner' is a process which runs a job. You can set up as many Runners as you need.") + = (_"Runners are processes that pick up and execute CI/CD jobs for GitLab.") %br - = _('Runners can be placed on separate users, servers, even on your local machine.') + = _('You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want.') %br %div - %span= _('Each Runner can be in one of the following states and/or belong to one of the following types:') + %span= _('Runners can be:') %ul %li %span.badge.badge-success shared \- - = _('Runner runs jobs from all unassigned projects') + = _('Runs jobs from all unassigned projects.') %li %span.badge.badge-success group \- - = _('Runner runs jobs from all unassigned projects in its group') + = _('Runs jobs from all unassigned projects in its group.') %li %span.badge.badge-info specific \- - = _('Runner runs jobs from assigned projects') + = _('Runs jobs from assigned projects.') %li %span.badge.badge-warning locked \- - = _('Runner cannot be assigned to other projects') + = _('Cannot be assigned to other projects.') %li %span.badge.badge-danger paused \- - = _('Runner will not receive any new jobs') + = _('Not available to run jobs.') .col-sm-6 .bs-callout diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml index 06925964dc5..508b1efd16a 100644 --- a/app/views/admin/runners/show.html.haml +++ b/app/views/admin/runners/show.html.haml @@ -15,17 +15,17 @@ - if @runner.instance_type? .bs-callout.bs-callout-success - %h4 This Runner will process jobs from ALL UNASSIGNED projects + %h4= _('This runner processes jobs for all unassigned projects.') %p - If you want Runners to build only specific projects, enable them in the table below. - Keep in mind that this is a one way transition. + = _('If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner.') - elsif @runner.group_type? .bs-callout.bs-callout-success - %h4 This runner will process jobs from all projects in its group and subgroups + %h4= _('This runner processes jobs for all projects in its group and subgroups.') - else .bs-callout.bs-callout-info - %h4 This Runner will process jobs only from ASSIGNED projects - %p You can't make this a shared Runner. + %h4= _('This runner processes jobs for assigned projects only.') + %p + = _('You cannot make this a shared runner.') %hr .gl-mb-6 @@ -33,12 +33,12 @@ .row .col-md-6 - %h4 Restrict projects for this Runner + %h4= _('Restrict projects for this runner') - if @runner.projects.any? %table.table.assigned-projects %thead %tr - %th Assigned projects + %th= _('Assigned projects') - @runner.runner_projects.each do |runner_project| - project = runner_project.project - if project @@ -55,7 +55,7 @@ %table.table.unassigned-projects %thead %tr - %th Project + %th= _('Project') %th %tr @@ -78,15 +78,15 @@ = paginate_without_count @projects .col-md-6 - %h4 Recent jobs served by this Runner + %h4= _('Recent jobs served by this runner') %table.table.ci-table.runner-builds %thead %tr - %th Job - %th Status - %th Project - %th Commit - %th Finished at + %th= _('Job') + %th= _('Status') + %th= _('Project') + %th= _('Commit') + %th= _('Finished at') - @builds.each do |build| - project = build.project diff --git a/app/views/projects/edit.html.haml b/app/views/projects/edit.html.haml index dffb57563ad..1dfb0594b27 100644 --- a/app/views/projects/edit.html.haml +++ b/app/views/projects/edit.html.haml @@ -3,8 +3,7 @@ - @content_class = "limit-container-width" unless fluid_layout - expanded = expanded_by_default? -- if Feature.enabled?(:search_settings_in_page, @project, default_enabled: false) - = render "shared/search_settings" += render "shared/search_settings" %section.settings.general-settings.no-animate.expanded#js-general-settings .settings-header diff --git a/app/views/shared/_search_settings.html.haml b/app/views/shared/_search_settings.html.haml index 9204e420b23..ea3d7b97327 100644 --- a/app/views/shared/_search_settings.html.haml +++ b/app/views/shared/_search_settings.html.haml @@ -1,3 +1,2 @@ -.search-box-by-type.gl-mt-5 - = sprite_icon('search', css_class: 'gl-search-box-by-type-search-icon gl-icon s16') - %input#search-settings-input.gl-form-input.gl-w-full.gl-search-box-by-type-input{ type: 'search', placeholder: _('Search settings') } +- if Feature.enabled?(:search_settings_in_page, @project, default_enabled: false) + .js-search-settings-app diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml index 675a8f922c4..26dcb1a732d 100644 --- a/app/views/shared/runners/_form.html.haml +++ b/app/views/shared/runners/_form.html.haml @@ -5,7 +5,7 @@ .col-sm-10 .form-check = f.check_box :active, { class: 'form-check-input' } - %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs") + %label.light{ for: :runner_active }= _("Paused runners don't accept new jobs") .form-group.row = label :protected, _("Protected"), class: 'col-form-label col-sm-2' .col-sm-10 @@ -40,13 +40,13 @@ = _('Maximum job timeout') .col-sm-10 = f.text_field :maximum_timeout_human_readable, class: 'form-control' - .form-text.text-muted= _('This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like "1 hour". Values without specification represent seconds.') + .form-text.text-muted= _('Enter the number of seconds, or other human-readable input, like "1 hour". This timeout takes precedence over lower timeouts set for the project.') .form-group.row = label_tag :tag_list, class: 'col-form-label col-sm-2' do = _('Tags') .col-sm-10 = f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control' - .form-text.text-muted= _('You can set up jobs to only use Runners with specific tags. Separate tags with commas.') + .form-text.text-muted= _('You can configure jobs to use runners that are assigned specific tags. Separate tags with commas.') - if local_assigns[:in_gitlab_com_admin_context] .form-group.row = label_tag :public_projects_minutes_cost_factor, class: 'col-form-label col-sm-2' do diff --git a/changelogs/unreleased/23308-allow-commits-to-fork-from-maintainers.yml b/changelogs/unreleased/23308-allow-commits-to-fork-from-maintainers.yml new file mode 100644 index 00000000000..98e0eb24395 --- /dev/null +++ b/changelogs/unreleased/23308-allow-commits-to-fork-from-maintainers.yml @@ -0,0 +1,5 @@ +--- +title: Allow collaboration on merge requests across forks by default +merge_request: 49904 +author: Jonston Chan @JonstonChan +type: changed diff --git a/changelogs/unreleased/selhorn-runner-lowercase-2.yml b/changelogs/unreleased/selhorn-runner-lowercase-2.yml new file mode 100644 index 00000000000..6b4e6b7a591 --- /dev/null +++ b/changelogs/unreleased/selhorn-runner-lowercase-2.yml @@ -0,0 +1,5 @@ +--- +title: Updated UI text to make runner lowercase +merge_request: 50477 +author: +type: other diff --git a/db/migrate/20201214000000_change_mr_allow_maintainer_to_push_default.rb b/db/migrate/20201214000000_change_mr_allow_maintainer_to_push_default.rb new file mode 100644 index 00000000000..0d97d54f3e4 --- /dev/null +++ b/db/migrate/20201214000000_change_mr_allow_maintainer_to_push_default.rb @@ -0,0 +1,17 @@ +class ChangeMrAllowMaintainerToPushDefault < ActiveRecord::Migration[6.0] + include Gitlab::Database::MigrationHelpers + + DOWNTIME = false + + def up + with_lock_retries do + change_column_default :merge_requests, :allow_maintainer_to_push, from: nil, to: true + end + end + + def down + with_lock_retries do + change_column_default :merge_requests, :allow_maintainer_to_push, from: true, to: nil + end + end +end diff --git a/db/schema_migrations/20201214000000 b/db/schema_migrations/20201214000000 new file mode 100644 index 00000000000..420b3f2768a --- /dev/null +++ b/db/schema_migrations/20201214000000 @@ -0,0 +1 @@ +58f2bd74adf8b4ef616c8f341053dbeaa8116430a0f4493cbf5f8456d7f4b907 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 5103ec06e32..8732dd74732 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -14018,7 +14018,7 @@ CREATE TABLE merge_requests ( merge_jid character varying, discussion_locked boolean, latest_merge_request_diff_id integer, - allow_maintainer_to_push boolean, + allow_maintainer_to_push boolean DEFAULT true, state_id smallint DEFAULT 1 NOT NULL, rebase_jid character varying, squash_commit_sha bytea, diff --git a/doc/user/project/merge_requests/allow_collaboration.md b/doc/user/project/merge_requests/allow_collaboration.md index 8eabef982c8..8adaae3b2ef 100644 --- a/doc/user/project/merge_requests/allow_collaboration.md +++ b/doc/user/project/merge_requests/allow_collaboration.md @@ -23,10 +23,10 @@ of the merge request. ## Enabling commit edits from upstream members -The feature can only be enabled by users who already have push access to the -source project and only lasts while the merge request is open. Once enabled, -upstream members will also be able to retry the pipelines and jobs of the -merge request: +From [GitLab 13.7 onwards](https://gitlab.com/gitlab-org/gitlab/-/issues/23308), +this setting is enabled by default. It can be changed by users with Developer +permissions to the source project. Once enabled, upstream members will also be +able to retry the pipelines and jobs of the merge request: 1. While creating or editing a merge request, select the checkbox **Allow commits from members who can merge to the target branch**. diff --git a/lib/gitlab/danger/roulette.rb b/lib/gitlab/danger/roulette.rb index 328083f7002..21feda2cf20 100644 --- a/lib/gitlab/danger/roulette.rb +++ b/lib/gitlab/danger/roulette.rb @@ -2,6 +2,8 @@ require_relative 'teammate' require_relative 'request_helper' unless defined?(Gitlab::Danger::RequestHelper) +require_relative 'weightage/reviewers' +require_relative 'weightage/maintainers' module Gitlab module Danger @@ -151,20 +153,14 @@ module Gitlab %i[reviewer traintainer maintainer].map do |role| spin_role_for_category(team, role, project, category) end - hungry_reviewers = reviewers.select { |member| member.hungry } - hungry_traintainers = traintainers.select { |member| member.hungry } - - # TODO: take CODEOWNERS into account? - # https://gitlab.com/gitlab-org/gitlab/issues/26723 random = new_random(mr_source_branch) - # Make hungry traintainers have 4x the chance to be picked as a reviewer - # Make traintainers have 3x the chance to be picked as a reviewer - # Make hungry reviewers have 2x the chance to be picked as a reviewer - weighted_reviewers = reviewers + hungry_reviewers + traintainers + traintainers + traintainers + hungry_traintainers + weighted_reviewers = Weightage::Reviewers.new(reviewers, traintainers).execute + weighted_maintainers = Weightage::Maintainers.new(maintainers).execute + reviewer = spin_for_person(weighted_reviewers, random: random, timezone_experiment: timezone_experiment) - maintainer = spin_for_person(maintainers, random: random, timezone_experiment: timezone_experiment) + maintainer = spin_for_person(weighted_maintainers, random: random, timezone_experiment: timezone_experiment) Spin.new(category, reviewer, maintainer, false, timezone_experiment) end diff --git a/lib/gitlab/danger/teammate.rb b/lib/gitlab/danger/teammate.rb index 287b12b7fd5..911b84d93ec 100644 --- a/lib/gitlab/danger/teammate.rb +++ b/lib/gitlab/danger/teammate.rb @@ -3,7 +3,7 @@ module Gitlab module Danger class Teammate - attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :tz_offset_hours + attr_reader :options, :username, :name, :role, :projects, :available, :hungry, :reduced_capacity, :tz_offset_hours # The options data are produced by https://gitlab.com/gitlab-org/gitlab-roulette/-/blob/master/lib/team_member.rb def initialize(options = {}) @@ -15,6 +15,7 @@ module Gitlab @projects = options['projects'] @available = options['available'] @hungry = options['hungry'] + @reduced_capacity = options['reduced_capacity'] @tz_offset_hours = options['tz_offset_hours'] end diff --git a/lib/gitlab/danger/weightage.rb b/lib/gitlab/danger/weightage.rb new file mode 100644 index 00000000000..67fade27573 --- /dev/null +++ b/lib/gitlab/danger/weightage.rb @@ -0,0 +1,10 @@ +# frozen_string_literal: true + +module Gitlab + module Danger + module Weightage + CAPACITY_MULTIPLIER = 2 # change this number to change what it means to be a reduced capacity reviewer 1/this number + BASE_REVIEWER_WEIGHT = 1 + end + end +end diff --git a/lib/gitlab/danger/weightage/maintainers.rb b/lib/gitlab/danger/weightage/maintainers.rb new file mode 100644 index 00000000000..cc0eb370e7a --- /dev/null +++ b/lib/gitlab/danger/weightage/maintainers.rb @@ -0,0 +1,33 @@ +# frozen_string_literal: true + +require_relative '../weightage' + +module Gitlab + module Danger + module Weightage + class Maintainers + def initialize(maintainers) + @maintainers = maintainers + end + + def execute + maintainers.each_with_object([]) do |maintainer, weighted_maintainers| + add_weighted_reviewer(weighted_maintainers, maintainer, BASE_REVIEWER_WEIGHT) + end + end + + private + + attr_reader :maintainers + + def add_weighted_reviewer(reviewers, reviewer, weight) + if reviewer.reduced_capacity + reviewers.fill(reviewer, reviewers.size, weight) + else + reviewers.fill(reviewer, reviewers.size, weight * CAPACITY_MULTIPLIER) + end + end + end + end + end +end diff --git a/lib/gitlab/danger/weightage/reviewers.rb b/lib/gitlab/danger/weightage/reviewers.rb new file mode 100644 index 00000000000..c8019be716e --- /dev/null +++ b/lib/gitlab/danger/weightage/reviewers.rb @@ -0,0 +1,65 @@ +# frozen_string_literal: true + +require_relative '../weightage' + +module Gitlab + module Danger + module Weightage + # Weights after (current multiplier of 2) + # + # +------------------------------+--------------------------------+ + # | reviewer type | weight(times in reviewer pool) | + # +------------------------------+--------------------------------+ + # | reduced capacity reviewer | 1 | + # | reviewer | 2 | + # | hungry reviewer | 4 | + # | reduced capacity traintainer | 3 | + # | traintainer | 6 | + # | hungry traintainer | 8 | + # +------------------------------+--------------------------------+ + # + class Reviewers + DEFAULT_REVIEWER_WEIGHT = CAPACITY_MULTIPLIER * BASE_REVIEWER_WEIGHT + TRAINTAINER_WEIGHT = 3 + + def initialize(reviewers, traintainers) + @reviewers = reviewers + @traintainers = traintainers + end + + def execute + # TODO: take CODEOWNERS into account? + # https://gitlab.com/gitlab-org/gitlab/issues/26723 + + weighted_reviewers + weighted_traintainers + end + + private + + attr_reader :reviewers, :traintainers + + def weighted_reviewers + reviewers.each_with_object([]) do |reviewer, total_reviewers| + add_weighted_reviewer(total_reviewers, reviewer, BASE_REVIEWER_WEIGHT) + end + end + + def weighted_traintainers + traintainers.each_with_object([]) do |reviewer, total_traintainers| + add_weighted_reviewer(total_traintainers, reviewer, TRAINTAINER_WEIGHT) + end + end + + def add_weighted_reviewer(reviewers, reviewer, weight) + if reviewer.reduced_capacity + reviewers.fill(reviewer, reviewers.size, weight) + elsif reviewer.hungry + reviewers.fill(reviewer, reviewers.size, weight * CAPACITY_MULTIPLIER + DEFAULT_REVIEWER_WEIGHT) + else + reviewers.fill(reviewer, reviewers.size, weight * CAPACITY_MULTIPLIER) + end + end + end + end + end +end diff --git a/locale/gitlab.pot b/locale/gitlab.pot index 7d1c64d960f..eae16815173 100644 --- a/locale/gitlab.pot +++ b/locale/gitlab.pot @@ -1205,9 +1205,6 @@ msgstr "" msgid "A %{incident_docs_start}modified issue%{incident_docs_end} to guide the resolution of incidents." msgstr "" -msgid "A 'Runner' is a process which runs a job. You can set up as many Runners as you need." -msgstr "" - msgid "A .NET Core console application template, customizable for any .NET Core project" msgstr "" @@ -3916,6 +3913,9 @@ msgstr "" msgid "Assigned Merge Requests" msgstr "" +msgid "Assigned projects" +msgstr "" + msgid "Assigned to %{assigneeName}" msgstr "" @@ -5086,6 +5086,9 @@ msgstr "" msgid "Cancelling Preview" msgstr "" +msgid "Cannot be assigned to other projects." +msgstr "" + msgid "Cannot be merged automatically" msgstr "" @@ -10263,9 +10266,6 @@ msgstr "" msgid "Dynamic Application Security Testing (DAST)" msgstr "" -msgid "Each Runner can be in one of the following states and/or belong to one of the following types:" -msgstr "" - msgid "Each Runner can be in one of the following states:" msgstr "" @@ -10791,6 +10791,9 @@ msgstr "" msgid "Enter the name of your application, and we'll return a unique %{type}." msgstr "" +msgid "Enter the number of seconds, or other human-readable input, like \"1 hour\". This timeout takes precedence over lower timeouts set for the project." +msgstr "" + msgid "Enter weights for storages for new repositories." msgstr "" @@ -12422,6 +12425,9 @@ msgstr "" msgid "Finished" msgstr "" +msgid "Finished at" +msgstr "" + msgid "First Seen" msgstr "" @@ -13085,9 +13091,6 @@ msgstr "" msgid "GitLab Billing Team." msgstr "" -msgid "GitLab Gold trial (optional)" -msgstr "" - msgid "GitLab Group Runners can execute code for all the projects in this group." msgstr "" @@ -14334,9 +14337,6 @@ msgstr "" msgid "How many days need to pass between marking entity for deletion and actual removing it." msgstr "" -msgid "How many employees will use Gitlab?" -msgstr "" - msgid "How many replicas each Elasticsearch shard has." msgstr "" @@ -14505,6 +14505,9 @@ msgstr "" msgid "If you remove this license, GitLab will fall back on the previous license, if any." msgstr "" +msgid "If you want a runner to build only specific projects, restrict the project in the table below. After you restrict a runner to a project, you cannot change it back to a shared runner." +msgstr "" + msgid "If you want to re-enable two-factor authentication, visit %{two_factor_link}" msgstr "" @@ -19223,6 +19226,9 @@ msgstr "" msgid "Not available for protected branches" msgstr "" +msgid "Not available to run jobs." +msgstr "" + msgid "Not confidential" msgstr "" @@ -20368,7 +20374,7 @@ msgstr "" msgid "Pause replication" msgstr "" -msgid "Paused Runners don't accept new jobs" +msgid "Paused runners don't accept new jobs" msgstr "" msgid "Pending" @@ -23080,6 +23086,9 @@ msgstr "" msgid "Recent Searches Service is unavailable" msgstr "" +msgid "Recent jobs served by this runner" +msgstr "" + msgid "Recent searches" msgstr "" @@ -24033,6 +24042,9 @@ msgstr "" msgid "Restrict membership by email domain" msgstr "" +msgid "Restrict projects for this runner" +msgstr "" + msgid "Restricts sign-ups for email addresses that match the given regex. See the %{supported_syntax_link_start}supported syntax%{supported_syntax_link_end} for more information." msgstr "" @@ -24187,21 +24199,9 @@ msgstr "" msgid "Run untagged jobs" msgstr "" -msgid "Runner cannot be assigned to other projects" -msgstr "" - msgid "Runner is %{status}, last contact was %{runner_contact} ago" msgstr "" -msgid "Runner runs jobs from all unassigned projects" -msgstr "" - -msgid "Runner runs jobs from all unassigned projects in its group" -msgstr "" - -msgid "Runner runs jobs from assigned projects" -msgstr "" - msgid "Runner token" msgstr "" @@ -24217,9 +24217,6 @@ msgstr "" msgid "Runner was successfully updated." msgstr "" -msgid "Runner will not receive any new jobs" -msgstr "" - msgid "Runners" msgstr "" @@ -24229,13 +24226,16 @@ msgstr "" msgid "Runners activated for this project" msgstr "" +msgid "Runners are processes that pick up and execute CI/CD jobs for GitLab." +msgstr "" + msgid "Runners are processes that pick up and execute jobs for GitLab. Here you can register and see your Runners for this project." msgstr "" msgid "Runners can be placed on separate users, servers, and even on your local machine." msgstr "" -msgid "Runners can be placed on separate users, servers, even on your local machine." +msgid "Runners can be:" msgstr "" msgid "Runners currently online: %{active_runners_count}" @@ -24313,6 +24313,15 @@ msgstr "" msgid "Runs a number of housekeeping tasks within the current repository, such as compressing file revisions and removing unreachable objects." msgstr "" +msgid "Runs jobs from all unassigned projects in its group." +msgstr "" + +msgid "Runs jobs from all unassigned projects." +msgstr "" + +msgid "Runs jobs from assigned projects." +msgstr "" + msgid "SAML" msgstr "" @@ -28851,6 +28860,15 @@ msgstr "" msgid "This repository was last checked %{last_check_timestamp}. The check passed." msgstr "" +msgid "This runner processes jobs for all projects in its group and subgroups." +msgstr "" + +msgid "This runner processes jobs for all unassigned projects." +msgstr "" + +msgid "This runner processes jobs for assigned projects only." +msgstr "" + msgid "This runner will only run on pipelines triggered on protected branches" msgstr "" @@ -28866,9 +28884,6 @@ msgstr "" msgid "This suggestion already matches its content." msgstr "" -msgid "This timeout will take precedence when lower than project-defined timeout and accepts a human readable time input language like \"1 hour\". Values without specification represent seconds." -msgstr "" - msgid "This user cannot be unlocked manually from GitLab" msgstr "" @@ -29673,12 +29688,39 @@ msgstr "" msgid "Trials|You won't get a free trial right now but you can always resume this process by clicking on your avatar and choosing 'Start a free trial'" msgstr "" +msgid "Trial|Company name" +msgstr "" + +msgid "Trial|Continue using the basic features of GitLab for free." +msgstr "" + +msgid "Trial|Country" +msgstr "" + msgid "Trial|Dismiss" msgstr "" +msgid "Trial|GitLab Gold trial (optional)" +msgstr "" + +msgid "Trial|How many employees will use Gitlab?" +msgstr "" + +msgid "Trial|Number of employees" +msgstr "" + msgid "Trial|Successful trial activation image" msgstr "" +msgid "Trial|Telephone number" +msgstr "" + +msgid "Trial|Upgrade to Gold to keep using GitLab with advanced features." +msgstr "" + +msgid "Trial|We will activate your trial on your group after you complete this step. After 30 days, you can:" +msgstr "" + msgid "Trigger" msgstr "" @@ -31321,9 +31363,6 @@ msgstr "" msgid "We want to be sure it is you, please confirm you are not a robot." msgstr "" -msgid "We will activate your trial on your group once you complete this step. After 30 days, you can upgrade to any plan" -msgstr "" - msgid "We will notify %{inviter} that you declined their invitation to join GitLab. You will stop receiving reminders." msgstr "" @@ -31941,6 +31980,9 @@ msgstr "" msgid "You can always edit this later" msgstr "" +msgid "You can configure jobs to use runners that are assigned specific tags. Separate tags with commas." +msgstr "" + msgid "You can create a new %{link}." msgstr "" @@ -32031,6 +32073,9 @@ msgstr "" msgid "You can recover this project until %{date}" msgstr "" +msgid "You can register runners as separate users, on separate servers, and on your local machine. Register as many runners as you want." +msgstr "" + msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}" msgstr "" @@ -32040,9 +32085,6 @@ msgstr "" msgid "You can set up as many Runners as you need to run your jobs." msgstr "" -msgid "You can set up jobs to only use Runners with specific tags. Separate tags with commas." -msgstr "" - msgid "You can specify notification level per group or per project." msgstr "" @@ -32064,6 +32106,9 @@ msgstr "" msgid "You cannot impersonate an internal user" msgstr "" +msgid "You cannot make this a shared runner." +msgstr "" + msgid "You cannot play this scheduled pipeline at the moment. Please wait a minute." msgstr "" diff --git a/spec/frontend/pages/projects/edit/mount_search_settings_spec.js b/spec/frontend/pages/projects/edit/mount_search_settings_spec.js new file mode 100644 index 00000000000..f966bfe495b --- /dev/null +++ b/spec/frontend/pages/projects/edit/mount_search_settings_spec.js @@ -0,0 +1,31 @@ +import waitForPromises from 'helpers/wait_for_promises'; +import { setHTMLFixture, resetHTMLFixture } from 'helpers/fixtures'; +import initSearch from '~/search_settings'; +import mountSearchSettings from '~/pages/projects/edit/mount_search_settings'; + +jest.mock('~/search_settings'); + +describe('pages/projects/edit/mount_search_settings', () => { + afterEach(() => { + initSearch.mockReset(); + resetHTMLFixture(); + }); + + it('initializes search settings when js-search-settings-app is available', async () => { + setHTMLFixture('
'); + + mountSearchSettings(); + + await waitForPromises(); + + expect(initSearch).toHaveBeenCalled(); + }); + + it('does not initialize search settings when js-search-settings-app is unavailable', async () => { + mountSearchSettings(); + + await waitForPromises(); + + expect(initSearch).not.toHaveBeenCalled(); + }); +}); diff --git a/spec/frontend/search_settings/components/search_settings_spec.js b/spec/frontend/search_settings/components/search_settings_spec.js new file mode 100644 index 00000000000..b80f9b15abf --- /dev/null +++ b/spec/frontend/search_settings/components/search_settings_spec.js @@ -0,0 +1,106 @@ +import { GlSearchBoxByType } from '@gitlab/ui'; +import { shallowMount } from '@vue/test-utils'; +import SearchSettings from '~/search_settings/components/search_settings.vue'; +import { HIGHLIGHT_CLASS, HIDE_CLASS } from '~/search_settings/constants'; + +describe('search_settings/components/search_settings.vue', () => { + const ROOT_ID = 'content-body'; + const SECTION_SELECTOR = 'section.settings'; + const SEARCH_TERM = 'Delete project'; + const GENERAL_SETTINGS_ID = 'js-general-settings'; + const ADVANCED_SETTINGS_ID = 'js-advanced-settings'; + let wrapper; + + const buildWrapper = () => { + wrapper = shallowMount(SearchSettings, { + propsData: { + searchRoot: document.querySelector(`#${ROOT_ID}`), + sectionSelector: SECTION_SELECTOR, + }, + }); + }; + + const sections = () => Array.from(document.querySelectorAll(SECTION_SELECTOR)); + const sectionsCount = () => sections().length; + const visibleSectionsCount = () => + document.querySelectorAll(`${SECTION_SELECTOR}:not(.${HIDE_CLASS})`).length; + const highlightedElementsCount = () => document.querySelectorAll(`.${HIGHLIGHT_CLASS}`).length; + const findSearchBox = () => wrapper.find(GlSearchBoxByType); + const search = (term) => { + findSearchBox().vm.$emit('input', term); + }; + const clearSearch = () => search(''); + + beforeEach(() => { + setFixtures(` +
+
+
+
+ General +
+
+ ${SEARCH_TERM} +
+
+
+ `); + buildWrapper(); + }); + + afterEach(() => { + wrapper.destroy(); + }); + + it('expands first section and collapses the rest', () => { + clearSearch(); + + const [firstSection, ...otherSections] = sections(); + + expect(wrapper.emitted()).toEqual({ + expand: [[firstSection]], + collapse: otherSections.map((x) => [x]), + }); + }); + + it('hides sections that do not match the search term', () => { + const hiddenSection = document.querySelector(`#${GENERAL_SETTINGS_ID}`); + search(SEARCH_TERM); + + expect(visibleSectionsCount()).toBe(1); + expect(hiddenSection.classList).toContain(HIDE_CLASS); + }); + + it('expands section that matches the search term', () => { + const section = document.querySelector(`#${ADVANCED_SETTINGS_ID}`); + + search(SEARCH_TERM); + + // Last called because expand is always called once to reset the page state + expect(wrapper.emitted().expand[1][0]).toBe(section); + }); + + it('highlight elements that match the search term', () => { + search(SEARCH_TERM); + + expect(highlightedElementsCount()).toBe(1); + }); + + describe('when search term is cleared', () => { + beforeEach(() => { + search(SEARCH_TERM); + }); + + it('displays all sections', () => { + expect(visibleSectionsCount()).toBe(1); + clearSearch(); + expect(visibleSectionsCount()).toBe(sectionsCount()); + }); + + it('removes the highlight from all elements', () => { + expect(highlightedElementsCount()).toBe(1); + clearSearch(); + expect(highlightedElementsCount()).toBe(0); + }); + }); +}); diff --git a/spec/frontend/search_settings/index_spec.js b/spec/frontend/search_settings/index_spec.js new file mode 100644 index 00000000000..122ee1251bb --- /dev/null +++ b/spec/frontend/search_settings/index_spec.js @@ -0,0 +1,36 @@ +import $ from 'jquery'; +import { setHTMLFixture } from 'helpers/fixtures'; +import initSearch from '~/search_settings'; +import { expandSection, closeSection } from '~/settings_panels'; + +jest.mock('~/settings_panels'); + +describe('search_settings/index', () => { + let app; + + beforeEach(() => { + const el = document.createElement('div'); + + setHTMLFixture('
'); + + app = initSearch({ el }); + }); + + afterEach(() => { + app.$destroy(); + }); + + it('calls settings_panel.onExpand when expand event is emitted', () => { + const section = { name: 'section' }; + app.$refs.searchSettings.$emit('expand', section); + + expect(expandSection).toHaveBeenCalledWith($(section)); + }); + + it('calls settings_panel.closeSection when collapse event is emitted', () => { + const section = { name: 'section' }; + app.$refs.searchSettings.$emit('collapse', section); + + expect(closeSection).toHaveBeenCalledWith($(section)); + }); +}); diff --git a/spec/lib/gitlab/danger/roulette_spec.rb b/spec/lib/gitlab/danger/roulette_spec.rb index 561e108bf31..59ac3b12b6b 100644 --- a/spec/lib/gitlab/danger/roulette_spec.rb +++ b/spec/lib/gitlab/danger/roulette_spec.rb @@ -245,69 +245,6 @@ RSpec.describe Gitlab::Danger::Roulette do end end end - - describe 'reviewer suggestion probability' do - let(:reviewer) { teammate_with_capability('reviewer', 'reviewer backend') } - let(:hungry_reviewer) { teammate_with_capability('hungry_reviewer', 'reviewer backend', hungry: true) } - let(:traintainer) { teammate_with_capability('traintainer', 'trainee_maintainer backend') } - let(:hungry_traintainer) { teammate_with_capability('hungry_traintainer', 'trainee_maintainer backend', hungry: true) } - let(:teammates) do - [ - reviewer.to_h, - hungry_reviewer.to_h, - traintainer.to_h, - hungry_traintainer.to_h - ] - end - - let(:categories) { [:backend] } - - # This test is testing probability with inherent randomness. - # The variance is inversely related to sample size - # Given large enough sample size, the variance would be smaller, - # but the test would take longer. - # Given smaller sample size, the variance would be larger, - # but the test would take less time. - let!(:sample_size) { 500 } - let!(:variance) { 0.1 } - - before do - # This test needs actual randomness to simulate probabilities - allow(subject).to receive(:new_random).and_return(Random.new) - WebMock - .stub_request(:get, described_class::ROULETTE_DATA_URL) - .to_return(body: teammate_json) - end - - it 'has 1:2:3:4 probability of picking reviewer, hungry_reviewer, traintainer, hungry_traintainer' do - picks = Array.new(sample_size).map do - spins = subject.spin(project, categories, timezone_experiment: timezone_experiment) - spins.first.reviewer.name - end - - expect(probability(picks, 'reviewer')).to be_within(variance).of(0.1) - expect(probability(picks, 'hungry_reviewer')).to be_within(variance).of(0.2) - expect(probability(picks, 'traintainer')).to be_within(variance).of(0.3) - expect(probability(picks, 'hungry_traintainer')).to be_within(variance).of(0.4) - end - - def probability(picks, role) - picks.count(role).to_f / picks.length - end - - def teammate_with_capability(name, capability, hungry: false) - Gitlab::Danger::Teammate.new( - { - 'name' => name, - 'projects' => { - 'gitlab' => capability - }, - 'available' => true, - 'hungry' => hungry - } - ) - end - end end RSpec::Matchers.define :match_teammates do |expected| diff --git a/spec/lib/gitlab/danger/weightage/maintainers_spec.rb b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb new file mode 100644 index 00000000000..066bb487fa2 --- /dev/null +++ b/spec/lib/gitlab/danger/weightage/maintainers_spec.rb @@ -0,0 +1,34 @@ +# frozen_string_literal: true + +require 'gitlab/danger/weightage/maintainers' + +RSpec.describe Gitlab::Danger::Weightage::Maintainers do + let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } + let(:regular_maintainer) { double('Teammate', reduced_capacity: false) } + let(:reduced_capacity_maintainer) { double('Teammate', reduced_capacity: true) } + let(:maintainers) do + [ + regular_maintainer, + reduced_capacity_maintainer + ] + end + + let(:maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } + let(:reduced_capacity_maintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } + + subject(:weighted_maintainers) { described_class.new(maintainers).execute } + + describe '#execute' do + it 'weights the maintainers overall' do + expect(weighted_maintainers.count).to eq maintainer_count + reduced_capacity_maintainer_count + end + + it 'has total count of regular maintainers' do + expect(weighted_maintainers.count { |r| r.object_id == regular_maintainer.object_id }).to eq maintainer_count + end + + it 'has count of reduced capacity maintainers' do + expect(weighted_maintainers.count { |r| r.object_id == reduced_capacity_maintainer.object_id }).to eq reduced_capacity_maintainer_count + end + end +end diff --git a/spec/lib/gitlab/danger/weightage/reviewers_spec.rb b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb new file mode 100644 index 00000000000..cca81f4d9b5 --- /dev/null +++ b/spec/lib/gitlab/danger/weightage/reviewers_spec.rb @@ -0,0 +1,63 @@ +# frozen_string_literal: true + +require 'gitlab/danger/weightage/reviewers' + +RSpec.describe Gitlab::Danger::Weightage::Reviewers do + let(:multiplier) { Gitlab::Danger::Weightage::CAPACITY_MULTIPLIER } + let(:regular_reviewer) { double('Teammate', hungry: false, reduced_capacity: false) } + let(:hungry_reviewer) { double('Teammate', hungry: true, reduced_capacity: false) } + let(:reduced_capacity_reviewer) { double('Teammate', hungry: false, reduced_capacity: true) } + let(:reviewers) do + [ + hungry_reviewer, + regular_reviewer, + reduced_capacity_reviewer + ] + end + + let(:regular_traintainer) { double('Teammate', hungry: false, reduced_capacity: false) } + let(:hungry_traintainer) { double('Teammate', hungry: true, reduced_capacity: false) } + let(:reduced_capacity_traintainer) { double('Teammate', hungry: false, reduced_capacity: true) } + let(:traintainers) do + [ + hungry_traintainer, + regular_traintainer, + reduced_capacity_traintainer + ] + end + + let(:hungry_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } + let(:hungry_traintainer_count) { described_class::TRAINTAINER_WEIGHT * multiplier + described_class::DEFAULT_REVIEWER_WEIGHT } + let(:reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * multiplier } + let(:traintainer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT * described_class::TRAINTAINER_WEIGHT * multiplier } + let(:reduced_capacity_reviewer_count) { Gitlab::Danger::Weightage::BASE_REVIEWER_WEIGHT } + let(:reduced_capacity_traintainer_count) { described_class::TRAINTAINER_WEIGHT } + + subject(:weighted_reviewers) { described_class.new(reviewers, traintainers).execute } + + describe '#execute', :aggregate_failures do + it 'weights the reviewers overall' do + reviewers_count = hungry_reviewer_count + reviewer_count + reduced_capacity_reviewer_count + traintainers_count = hungry_traintainer_count + traintainer_count + reduced_capacity_traintainer_count + + expect(weighted_reviewers.count).to eq reviewers_count + traintainers_count + end + + it 'has total count of hungry reviewers and traintainers' do + expect(weighted_reviewers.count(&:hungry)).to eq hungry_reviewer_count + hungry_traintainer_count + expect(weighted_reviewers.count { |r| r.object_id == hungry_reviewer.object_id }).to eq hungry_reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == hungry_traintainer.object_id }).to eq hungry_traintainer_count + end + + it 'has total count of regular reviewers and traintainers' do + expect(weighted_reviewers.count { |r| r.object_id == regular_reviewer.object_id }).to eq reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == regular_traintainer.object_id }).to eq traintainer_count + end + + it 'has count of reduced capacity reviewers' do + expect(weighted_reviewers.count(&:reduced_capacity)).to eq reduced_capacity_reviewer_count + reduced_capacity_traintainer_count + expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_reviewer.object_id }).to eq reduced_capacity_reviewer_count + expect(weighted_reviewers.count { |r| r.object_id == reduced_capacity_traintainer.object_id }).to eq reduced_capacity_traintainer_count + end + end +end diff --git a/spec/lib/gitlab/git_access_spec.rb b/spec/lib/gitlab/git_access_spec.rb index 42b64687fc1..a0cafe3d763 100644 --- a/spec/lib/gitlab/git_access_spec.rb +++ b/spec/lib/gitlab/git_access_spec.rb @@ -891,6 +891,10 @@ RSpec.describe Gitlab::GitAccess do # Expectations are given a custom failure message proc so that it's # easier to identify which check(s) failed. it "has the correct permissions for #{role}s" do + if role == :admin_without_admin_mode + skip("All admins are allowed to perform actions https://gitlab.com/gitlab-org/gitlab/-/issues/296509") + end + if [:admin_with_admin_mode, :admin_without_admin_mode].include?(role) user.update_attribute(:admin, true) enable_admin_mode!(user) if role == :admin_with_admin_mode diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb index f0c84898d67..8e34a303956 100644 --- a/spec/models/ci/build_spec.rb +++ b/spec/models/ci/build_spec.rb @@ -4360,7 +4360,7 @@ RSpec.describe Ci::Build do end describe '#supported_runner?' do - let_it_be(:build) { create(:ci_build) } + let_it_be_with_refind(:build) { create(:ci_build) } subject { build.supported_runner?(runner_features) } @@ -4425,6 +4425,41 @@ RSpec.describe Ci::Build do it { is_expected.to be_falsey } end end + + context 'when `return_exit_code` feature is required by build' do + let(:options) { { allow_failure_criteria: { exit_codes: [1] } } } + + before do + build.update!(options: options) + end + + context 'when runner provides given feature' do + let(:runner_features) { { return_exit_code: true } } + + it { is_expected.to be_truthy } + end + + context 'when runner does not provide given feature' do + let(:runner_features) { {} } + + it { is_expected.to be_falsey } + end + + context 'when the runner does not provide all of the required features' do + let(:options) do + { + allow_failure_criteria: { exit_codes: [1] }, + artifacts: { reports: { junit: "junit.xml" } } + } + end + + let(:runner_features) { { return_exit_code: true } } + + it 'requires `upload_multiple_artifacts` too' do + is_expected.to be_falsey + end + end + end end describe '#deployment_status' do @@ -4927,4 +4962,56 @@ RSpec.describe Ci::Build do end end end + + describe '#exit_codes_defined?' do + let(:options) { {} } + + before do + build.options.merge!(options) + end + + subject(:exit_codes_defined) do + build.exit_codes_defined? + end + + context 'without allow_failure_criteria' do + it { is_expected.to be_falsey } + end + + context 'when exit_codes is nil' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: nil + } + } + end + + it { is_expected.to be_falsey } + end + + context 'when exit_codes is an empty array' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [] + } + } + end + + it { is_expected.to be_falsey } + end + + context 'when exit_codes are defined' do + let(:options) do + { + allow_failure_criteria: { + exit_codes: [5, 6] + } + } + end + + it { is_expected.to be_truthy } + end + end end diff --git a/spec/services/merge_requests/update_service_spec.rb b/spec/services/merge_requests/update_service_spec.rb index fdda1515cce..9eb82dcd0ad 100644 --- a/spec/services/merge_requests/update_service_spec.rb +++ b/spec/services/merge_requests/update_service_spec.rb @@ -842,20 +842,20 @@ RSpec.describe MergeRequests::UpdateService, :mailer do it 'does not allow a maintainer of the target project to set `allow_collaboration`' do target_project.add_developer(user) - update_merge_request(allow_collaboration: true, title: 'Updated title') + update_merge_request(allow_collaboration: false, title: 'Updated title') expect(merge_request.title).to eq('Updated title') - expect(merge_request.allow_collaboration).to be_falsy + expect(merge_request.allow_collaboration).to be_truthy end it 'is allowed by a user that can push to the source and can update the merge request' do merge_request.update!(assignees: [user]) source_project.add_developer(user) - update_merge_request(allow_collaboration: true, title: 'Updated title') + update_merge_request(allow_collaboration: false, title: 'Updated title') expect(merge_request.title).to eq('Updated title') - expect(merge_request.allow_collaboration).to be_truthy + expect(merge_request.allow_collaboration).to be_falsy end end