summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorGitLab Bot <gitlab-bot@gitlab.com>2020-05-15 12:08:28 +0000
committerGitLab Bot <gitlab-bot@gitlab.com>2020-05-15 12:08:28 +0000
commit927df95cc4453bdacbc59960df32008b02c4e28a (patch)
tree24c2af26a0b7a06661bdf077c3d0300684602e39
parente5731d5194e20deb33725943248c5899e4fdf05d (diff)
downloadgitlab-ce-927df95cc4453bdacbc59960df32008b02c4e28a.tar.gz
Add latest changes from gitlab-org/gitlab@master
-rw-r--r--.rubocop_todo.yml11
-rw-r--r--app/assets/javascripts/alert_management/components/alert_management_list.vue4
-rw-r--r--app/assets/javascripts/header.js23
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue26
-rw-r--r--app/assets/javascripts/monitoring/stores/getters.js4
-rw-r--r--app/assets/javascripts/notes/components/note_header.vue9
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue27
-rw-r--r--app/assets/stylesheets/framework/header.scss1
-rw-r--r--app/finders/freeze_periods_finder.rb14
-rw-r--r--app/models/ci/freeze_period.rb2
-rw-r--r--app/models/group.rb7
-rw-r--r--app/models/milestone.rb8
-rw-r--r--app/policies/ci/freeze_period_policy.rb7
-rw-r--r--app/policies/project_policy.rb4
-rw-r--r--app/services/clusters/management/create_project_service.rb7
-rw-r--r--app/services/merge_requests/base_service.rb2
-rw-r--r--app/services/projects/create_service.rb16
-rw-r--r--app/services/user_project_access_changed_service.rb13
-rw-r--r--app/views/layouts/header/_current_user_dropdown.html.haml1
-rw-r--r--app/views/shared/_auto_devops_callout.html.haml2
-rw-r--r--app/views/shared/_clone_panel.html.haml2
-rw-r--r--app/views/shared/_mini_pipeline_graph.html.haml2
-rw-r--r--app/views/shared/_no_ssh.html.haml2
-rw-r--r--app/views/shared/issuable/_nav.html.haml8
-rw-r--r--changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml5
-rw-r--r--changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml6
-rw-r--r--changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml5
-rw-r--r--changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml5
-rw-r--r--changelogs/unreleased/24295-freeze-period-API-changes.yml5
-rw-r--r--changelogs/unreleased/ab-fix-not-null-inconsistency.yml5
-rw-r--r--db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb17
-rw-r--r--db/migrate/20200409105456_add_checksum_index_to_package_file.rb17
-rw-r--r--db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb20
-rw-r--r--db/structure.sql7
-rw-r--r--doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md6
-rw-r--r--doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md4
-rw-r--r--doc/api/merge_requests.md10
-rw-r--r--doc/development/code_review.md6
-rw-r--r--doc/development/geo/framework.md2
-rw-r--r--doc/development/testing_guide/best_practices.md4
-rw-r--r--doc/install/requirements.md6
-rw-r--r--doc/user/group/epics/img/epic_view_v13.0.pngbin68803 -> 54891 bytes
-rw-r--r--doc/user/project/static_site_editor/index.md6
-rw-r--r--doc/user/snippets.md4
-rw-r--r--lib/api/api.rb1
-rw-r--r--lib/api/entities/freeze_period.rb11
-rw-r--r--lib/api/entities/merge_request_basic.rb6
-rw-r--r--lib/api/freeze_periods.rb107
-rw-r--r--lib/api/groups.rb2
-rw-r--r--lib/api/helpers/merge_requests_helpers.rb1
-rw-r--r--lib/api/merge_requests.rb3
-rw-r--r--lib/container_registry/client.rb17
-rw-r--r--lib/gitlab/experimentation.rb3
-rw-r--r--lib/gitlab/git/commit.rb14
-rw-r--r--lib/gitlab/usage_data.rb51
-rw-r--r--locale/gitlab.pot30
-rw-r--r--package.json2
-rw-r--r--spec/controllers/repositories/git_http_controller_spec.rb3
-rw-r--r--spec/finders/freeze_periods_finder_spec.rb59
-rw-r--r--spec/finders/projects/serverless/functions_finder_spec.rb1
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/freeze_period.json20
-rw-r--r--spec/fixtures/api/schemas/public_api/v4/freeze_periods.json5
-rw-r--r--spec/frontend/header_spec.js16
-rw-r--r--spec/frontend/monitoring/components/dashboard_spec.js69
-rw-r--r--spec/frontend/monitoring/components/dashboard_template_spec.js3
-rw-r--r--spec/frontend/monitoring/store_utils.js11
-rw-r--r--spec/frontend/notes/components/note_header_spec.js15
-rw-r--r--spec/frontend/releases/stores/modules/detail/mutations_spec.js24
-rw-r--r--spec/lib/container_registry/client_spec.rb52
-rw-r--r--spec/lib/gitlab/git/commit_spec.rb20
-rw-r--r--spec/lib/gitlab/hook_data/issuable_builder_spec.rb1
-rw-r--r--spec/lib/gitlab/legacy_github_import/importer_spec.rb1
-rw-r--r--spec/lib/gitlab/usage_data_spec.rb77
-rw-r--r--spec/models/ci/freeze_period_spec.rb14
-rw-r--r--spec/models/event_spec.rb8
-rw-r--r--spec/models/milestone_spec.rb18
-rw-r--r--spec/requests/api/freeze_periods_spec.rb475
-rw-r--r--spec/requests/api/groups_spec.rb40
-rw-r--r--spec/requests/api/merge_requests_spec.rb29
-rw-r--r--spec/services/projects/create_service_spec.rb77
-rw-r--r--spec/services/user_project_access_changed_service_spec.rb9
-rw-r--r--spec/support/helpers/usage_data_helpers.rb37
-rw-r--r--spec/support/shared_examples/features/error_tracking_shared_example.rb14
-rw-r--r--spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb1
-rw-r--r--yarn.lock8
85 files changed, 1402 insertions, 295 deletions
diff --git a/.rubocop_todo.yml b/.rubocop_todo.yml
index c359670759e..bd0f9184cd6 100644
--- a/.rubocop_todo.yml
+++ b/.rubocop_todo.yml
@@ -208,17 +208,6 @@ Naming/RescuedExceptionsVariableName:
RSpec/ContextWording:
Enabled: false
-RSpec/EmptyLineAfterSharedExample:
- Exclude:
- - 'ee/spec/mailers/notify_spec.rb'
- - 'ee/spec/services/quick_actions/interpret_service_spec.rb'
- - 'spec/controllers/repositories/git_http_controller_spec.rb'
- - 'spec/finders/projects/serverless/functions_finder_spec.rb'
- - 'spec/lib/gitlab/hook_data/issuable_builder_spec.rb'
- - 'spec/lib/gitlab/legacy_github_import/importer_spec.rb'
- - 'spec/models/event_spec.rb'
- - 'spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb'
-
# Offense count: 879
# Cop supports --auto-correct.
# Configuration parameters: EnforcedStyle.
diff --git a/app/assets/javascripts/alert_management/components/alert_management_list.vue b/app/assets/javascripts/alert_management/components/alert_management_list.vue
index 857e5717045..b9edbb4b0a0 100644
--- a/app/assets/javascripts/alert_management/components/alert_management_list.vue
+++ b/app/assets/javascripts/alert_management/components/alert_management_list.vue
@@ -148,7 +148,7 @@ export default {
},
},
methods: {
- filterALertsByStatus(tabIndex) {
+ filterAlertsByStatus(tabIndex) {
this.statusFilter = this.$options.statusTabs[tabIndex].filters;
},
capitalizeFirstCharacter,
@@ -184,7 +184,7 @@ export default {
{{ $options.i18n.errorMsg }}
</gl-alert>
- <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterALertsByStatus">
+ <gl-tabs v-if="glFeatures.alertListStatusFilteringEnabled" @input="filterAlertsByStatus">
<gl-tab v-for="tab in $options.statusTabs" :key="tab.status">
<template slot="title">
<span>{{ tab.title }}</span>
diff --git a/app/assets/javascripts/header.js b/app/assets/javascripts/header.js
index 1678991b1ea..67b068f1c6b 100644
--- a/app/assets/javascripts/header.js
+++ b/app/assets/javascripts/header.js
@@ -74,20 +74,27 @@ function initStatusTriggers() {
}
}
+function trackShowUserDropdownLink(trackEvent, elToTrack, el) {
+ const { trackLabel, trackProperty } = elToTrack.dataset;
+
+ $(el).on('shown.bs.dropdown', () => {
+ Tracking.event(document.body.dataset.page, trackEvent, {
+ label: trackLabel,
+ property: trackProperty,
+ });
+ });
+}
export function initNavUserDropdownTracking() {
const el = document.querySelector('.js-nav-user-dropdown');
const buyEl = document.querySelector('.js-buy-ci-minutes-link');
+ const upgradeEl = document.querySelector('.js-upgrade-plan-link');
if (el && buyEl) {
- const { trackLabel, trackProperty } = buyEl.dataset;
- const trackEvent = 'show_buy_ci_minutes';
+ trackShowUserDropdownLink('show_buy_ci_minutes', buyEl, el);
+ }
- $(el).on('shown.bs.dropdown', () => {
- Tracking.event(undefined, trackEvent, {
- label: trackLabel,
- property: trackProperty,
- });
- });
+ if (el && upgradeEl) {
+ trackShowUserDropdownLink('show_upgrade_link', upgradeEl, el);
}
}
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 02f4e26ba3b..de73da67fcc 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -228,13 +228,11 @@ export default {
'promVariables',
'isUpdatingStarredValue',
]),
- ...mapGetters('monitoringDashboard', ['getMetricStates', 'filteredEnvironments']),
- firstDashboard() {
- return this.allDashboards.length > 0 ? this.allDashboards[0] : {};
- },
- selectedDashboard() {
- return this.allDashboards.find(d => d.path === this.currentDashboard) || this.firstDashboard;
- },
+ ...mapGetters('monitoringDashboard', [
+ 'selectedDashboard',
+ 'getMetricStates',
+ 'filteredEnvironments',
+ ]),
showRearrangePanelsBtn() {
return !this.showEmptyState && this.rearrangePanelsAvailable;
},
@@ -242,7 +240,10 @@ export default {
return (
this.customMetricsAvailable &&
!this.showEmptyState &&
- this.firstDashboard === this.selectedDashboard
+ // Custom metrics only avaialble on system dashboards because
+ // they are stored in the database. This can be improved. See:
+ // https://gitlab.com/gitlab-org/gitlab/-/issues/28241
+ this.selectedDashboard?.system_dashboard
);
},
shouldShowEnvironmentsDropdownNoMatchedMsg() {
@@ -269,7 +270,7 @@ export default {
},
expandedPanel: {
handler({ group, panel }) {
- const dashboardPath = this.currentDashboard || this.firstDashboard.path;
+ const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
updateHistory({
url: panelToUrl(dashboardPath, group, panel),
title: document.title,
@@ -341,7 +342,7 @@ export default {
this.selectedTimeRange = defaultTimeRange;
},
generatePanelUrl(groupKey, panel) {
- const dashboardPath = this.currentDashboard || this.firstDashboard.path;
+ const dashboardPath = this.currentDashboard || this.selectedDashboard?.path;
return panelToUrl(dashboardPath, groupKey, panel);
},
hideAddMetricModal() {
@@ -597,7 +598,10 @@ export default {
</gl-modal>
</div>
- <div v-if="selectedDashboard.can_edit" class="mb-2 mr-2 d-flex d-sm-block">
+ <div
+ v-if="selectedDashboard && selectedDashboard.can_edit"
+ class="mb-2 mr-2 d-flex d-sm-block"
+ >
<gl-deprecated-button
class="flex-grow-1 js-edit-link"
:href="selectedDashboard.project_blob_path"
diff --git a/app/assets/javascripts/monitoring/stores/getters.js b/app/assets/javascripts/monitoring/stores/getters.js
index 12757bed588..63a7d0c643d 100644
--- a/app/assets/javascripts/monitoring/stores/getters.js
+++ b/app/assets/javascripts/monitoring/stores/getters.js
@@ -14,7 +14,9 @@ const metricsIdsInPanel = panel =>
export const selectedDashboard = state => {
const { allDashboards } = state;
return (
- allDashboards.find(({ path }) => path === state.currentDashboard) || allDashboards[0] || null
+ allDashboards.find(d => d.path === state.currentDashboard) ||
+ allDashboards.find(d => d.default) ||
+ null
);
};
diff --git a/app/assets/javascripts/notes/components/note_header.vue b/app/assets/javascripts/notes/components/note_header.vue
index 74a0b69bc54..a323e08d26d 100644
--- a/app/assets/javascripts/notes/components/note_header.vue
+++ b/app/assets/javascripts/notes/components/note_header.vue
@@ -1,12 +1,12 @@
<script>
import { mapActions } from 'vuex';
import timeAgoTooltip from '~/vue_shared/components/time_ago_tooltip.vue';
-import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
export default {
components: {
timeAgoTooltip,
- GitlabTeamMemberBadge,
+ GitlabTeamMemberBadge: () =>
+ import('ee_component/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue'),
},
props: {
author: {
@@ -62,9 +62,6 @@ export default {
hasAuthor() {
return this.author && Object.keys(this.author).length;
},
- showGitlabTeamMemberBadge() {
- return this.author?.is_gitlab_employee;
- },
authorLinkClasses() {
return {
hover: this.isUsernameLinkHovered,
@@ -156,7 +153,7 @@ export default {
@mouseleave="handleUsernameMouseLeave"
><span class="note-headline-light">@{{ author.username }}</span>
</a>
- <gitlab-team-member-badge v-if="showGitlabTeamMemberBadge" />
+ <gitlab-team-member-badge v-if="author && author.is_gitlab_employee" />
</span>
</template>
<span v-else>{{ __('A deleted user') }}</span>
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue b/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
deleted file mode 100644
index 527cbd458e2..00000000000
--- a/app/assets/javascripts/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue
+++ /dev/null
@@ -1,27 +0,0 @@
-<script>
-import { GlTooltipDirective, GlIcon } from '@gitlab/ui';
-import { __ } from '~/locale';
-
-const GITLAB_TEAM_MEMBER_LABEL = __('GitLab Team Member');
-
-export default {
- name: 'GitlabTeamMemberBadge',
- directives: {
- GlTooltip: GlTooltipDirective,
- },
- components: { GlIcon },
- gitlabTeamMemberLabel: GITLAB_TEAM_MEMBER_LABEL,
-};
-</script>
-
-<template>
- <span
- v-gl-tooltip.hover
- :title="$options.gitlabTeamMemberLabel"
- role="img"
- :aria-label="$options.gitlabTeamMemberLabel"
- class="d-inline-block align-middle"
- >
- <gl-icon name="tanuki-verified" class="gl-text-purple d-block" />
- </span>
-</template>
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a0a020ec548..2c7e9428ef1 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -553,6 +553,7 @@
vertical-align: text-top;
}
+ a.upgrade-plan-link gl-emoji,
a.ci-minutes-emoji gl-emoji,
a.trial-link gl-emoji {
font-size: $gl-font-size;
diff --git a/app/finders/freeze_periods_finder.rb b/app/finders/freeze_periods_finder.rb
new file mode 100644
index 00000000000..2a9bfbe12ba
--- /dev/null
+++ b/app/finders/freeze_periods_finder.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class FreezePeriodsFinder
+ def initialize(project, current_user = nil)
+ @project = project
+ @current_user = current_user
+ end
+
+ def execute
+ return Ci::FreezePeriod.none unless Ability.allowed?(@current_user, :read_freeze_period, @project)
+
+ @project.freeze_periods
+ end
+end
diff --git a/app/models/ci/freeze_period.rb b/app/models/ci/freeze_period.rb
index 3875ab5d41b..bf03b92259a 100644
--- a/app/models/ci/freeze_period.rb
+++ b/app/models/ci/freeze_period.rb
@@ -5,6 +5,8 @@ module Ci
include StripAttribute
self.table_name = 'ci_freeze_periods'
+ default_scope { order(created_at: :asc) }
+
belongs_to :project, inverse_of: :freeze_periods
strip_attributes :freeze_start, :freeze_end
diff --git a/app/models/group.rb b/app/models/group.rb
index 24e3dce9b5d..04cb6b8b4da 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -305,9 +305,10 @@ class Group < Namespace
# rubocop: enable CodeReuse/ServiceClass
# rubocop: disable CodeReuse/ServiceClass
- def refresh_members_authorized_projects(blocking: true)
- UserProjectAccessChangedService.new(user_ids_for_project_authorizations)
- .execute(blocking: blocking)
+ def refresh_members_authorized_projects(blocking: true, priority: UserProjectAccessChangedService::HIGH_PRIORITY)
+ UserProjectAccessChangedService
+ .new(user_ids_for_project_authorizations)
+ .execute(blocking: blocking, priority: priority)
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/milestone.rb b/app/models/milestone.rb
index e481d3ae398..b5e4f62792e 100644
--- a/app/models/milestone.rb
+++ b/app/models/milestone.rb
@@ -171,6 +171,14 @@ class Milestone < ApplicationRecord
alias_method :group_milestone?, :group_timebox?
alias_method :project_milestone?, :project_timebox?
+ def parent
+ if group_milestone?
+ group
+ else
+ project
+ end
+ end
+
private
def milestone_format_reference(format = :iid)
diff --git a/app/policies/ci/freeze_period_policy.rb b/app/policies/ci/freeze_period_policy.rb
new file mode 100644
index 00000000000..60e53a7b2f9
--- /dev/null
+++ b/app/policies/ci/freeze_period_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class FreezePeriodPolicy < BasePolicy
+ delegate { @subject.resource_parent }
+ end
+end
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index 1529ff07005..7f809ca88c1 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -362,6 +362,10 @@ class ProjectPolicy < BasePolicy
enable :destroy_deploy_token
enable :read_prometheus_alerts
enable :admin_terraform_state
+ enable :create_freeze_period
+ enable :read_freeze_period
+ enable :update_freeze_period
+ enable :destroy_freeze_period
end
rule { public_project & metrics_dashboard_allowed }.policy do
diff --git a/app/services/clusters/management/create_project_service.rb b/app/services/clusters/management/create_project_service.rb
index 0a33582be98..5a0176edd12 100644
--- a/app/services/clusters/management/create_project_service.rb
+++ b/app/services/clusters/management/create_project_service.rb
@@ -15,11 +15,8 @@ module Clusters
def execute
return unless management_project_required?
- ActiveRecord::Base.transaction do
- project = create_management_project!
-
- update_cluster!(project)
- end
+ project = create_management_project!
+ update_cluster!(project)
end
private
diff --git a/app/services/merge_requests/base_service.rb b/app/services/merge_requests/base_service.rb
index b250c2d7d21..7f7bfa29af7 100644
--- a/app/services/merge_requests/base_service.rb
+++ b/app/services/merge_requests/base_service.rb
@@ -39,6 +39,8 @@ module MergeRequests
# Don't try to print expensive instance variables.
def inspect
+ return "#<#{self.class}>" unless respond_to?(:merge_request)
+
"#<#{self.class} #{merge_request.to_reference(full: true)}>"
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index 57f5acbc337..3233d1799b8 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -108,8 +108,22 @@ module Projects
# users in the background
def setup_authorizations
if @project.group
- @project.group.refresh_members_authorized_projects(blocking: false)
current_user.refresh_authorized_projects
+
+ if Feature.enabled?(:specialized_project_authorization_workers)
+ AuthorizedProjectUpdate::ProjectCreateWorker.perform_async(@project.id)
+ # AuthorizedProjectsWorker uses an exclusive lease per user but
+ # specialized workers might have synchronization issues. Until we
+ # compare the inconsistency rates of both approaches, we still run
+ # AuthorizedProjectsWorker but with some delay and lower urgency as a
+ # safety net.
+ @project.group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
+ else
+ @project.group.refresh_members_authorized_projects(blocking: false)
+ end
else
@project.add_maintainer(@project.namespace.owner, current_user: current_user)
end
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 21d0861ac3f..66f1ccfab70 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -1,17 +1,26 @@
# frozen_string_literal: true
class UserProjectAccessChangedService
+ DELAY = 1.hour
+
+ HIGH_PRIORITY = :high
+ LOW_PRIORITY = :low
+
def initialize(user_ids)
@user_ids = Array.wrap(user_ids)
end
- def execute(blocking: true)
+ def execute(blocking: true, priority: HIGH_PRIORITY)
bulk_args = @user_ids.map { |id| [id] }
if blocking
AuthorizedProjectsWorker.bulk_perform_and_wait(bulk_args)
else
- AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ if priority == HIGH_PRIORITY
+ AuthorizedProjectsWorker.bulk_perform_async(bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ else
+ AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker.bulk_perform_in(DELAY, bulk_args) # rubocop:disable Scalability/BulkPerformWithContext
+ end
end
end
end
diff --git a/app/views/layouts/header/_current_user_dropdown.html.haml b/app/views/layouts/header/_current_user_dropdown.html.haml
index 410b120396d..7d9924719a2 100644
--- a/app/views/layouts/header/_current_user_dropdown.html.haml
+++ b/app/views/layouts/header/_current_user_dropdown.html.haml
@@ -27,6 +27,7 @@
%li
= link_to s_("CurrentUser|Settings"), profile_path, data: { qa_selector: 'settings_link' }
= render_if_exists 'layouts/header/buy_ci_minutes', project: @project, namespace: @group
+ = render_if_exists 'layouts/header/upgrade'
- if current_user_menu?(:help)
%li.divider.d-md-none
diff --git a/app/views/shared/_auto_devops_callout.html.haml b/app/views/shared/_auto_devops_callout.html.haml
index 128508e954e..bf1683be32d 100644
--- a/app/views/shared/_auto_devops_callout.html.haml
+++ b/app/views/shared/_auto_devops_callout.html.haml
@@ -11,5 +11,5 @@
= link_to s_('AutoDevOps|Enable in settings'), project_settings_ci_cd_path(@project, anchor: 'autodevops-settings'), class: 'btn btn-md new-gl-button js-close-callout'
%button.gl-banner-close.close.js-close-callout{ type: 'button',
- 'aria-label' => 'Dismiss Auto DevOps box' }
+ 'aria-label' => s_('AutoDevOps|Dismiss Auto DevOps box') }
= icon('times', class: 'dismiss-icon', 'aria-hidden' => 'true')
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index 3e805189055..9ec8d3c18cd 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -18,7 +18,7 @@
= http_clone_button(project)
= render_if_exists 'shared/kerberos_clone_button', project: project
- = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
+ = text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: _('Project clone URL') }
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL"), class: "input-group-text btn-default btn-clipboard")
diff --git a/app/views/shared/_mini_pipeline_graph.html.haml b/app/views/shared/_mini_pipeline_graph.html.haml
index a1f21c2a83e..172f3d85472 100644
--- a/app/views/shared/_mini_pipeline_graph.html.haml
+++ b/app/views/shared/_mini_pipeline_graph.html.haml
@@ -14,4 +14,4 @@
%li.js-builds-dropdown-loading.hidden
.loading-container.text-center
- %span.spinner{ 'aria-label': 'Loading' }
+ %span.spinner{ 'aria-label': _('Loading') }
diff --git a/app/views/shared/_no_ssh.html.haml b/app/views/shared/_no_ssh.html.haml
index fbfd4d0e9a9..2b04e3e1c98 100644
--- a/app/views/shared/_no_ssh.html.haml
+++ b/app/views/shared/_no_ssh.html.haml
@@ -1,7 +1,7 @@
- if show_no_ssh_key_message?
%div{ class: 'no-ssh-key-message gl-alert gl-alert-warning', role: 'alert' }
= sprite_icon('warning', size: 16, css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
- %button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': 'Dismiss' }
+ %button{ class: 'gl-alert-dismiss hide-no-ssh-message', type: 'button', 'aria-label': _('Dismiss') }
= sprite_icon('close', size: 16, css_class: 'gl-icon s16')
.gl-alert-body
= s_("MissingSSHKeyWarningLink|You won't be able to pull or push project code via SSH until you add an SSH key to your profile").html_safe
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index 93408e0bfc0..c715cd8f736 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -4,20 +4,20 @@
%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
%li{ class: active_when(params[:state] == 'opened') }>
- = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: "Filter by #{page_context_word} that are currently opened.", data: { state: 'opened' } do
+ = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: _("Filter by %{page_context_word} that are currently opened.") % { page_context_word: page_context_word }, data: { state: 'opened' } do
#{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
%li{ class: active_when(params[:state] == 'merged') }>
- = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: 'Filter by merge requests that are currently merged.', data: { state: 'merged' } do
+ = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: _('Filter by merge requests that are currently merged.'), data: { state: 'merged' } do
#{issuables_state_counter_text(type, :merged, display_count)}
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by merge requests that are currently closed and unmerged.', data: { state: 'closed' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
#{issuables_state_counter_text(type, :closed, display_count)}
- else
%li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: 'Filter by issues that are currently closed.', data: { state: 'closed', qa_selector: 'closed_issues_link' } do
+ = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
#{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml b/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml
new file mode 100644
index 00000000000..af0cfa4b80b
--- /dev/null
+++ b/changelogs/unreleased/217455-skip-mergeability-check-on-list-endpoint-by-default.yml
@@ -0,0 +1,5 @@
+---
+title: Skip mergeability check when listing MRs in the API
+merge_request: 31890
+author:
+type: performance
diff --git a/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml b/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml
new file mode 100644
index 00000000000..550d679a99e
--- /dev/null
+++ b/changelogs/unreleased/217708-nomethoderror-undefined-method-admin-for-nil-nilclass.yml
@@ -0,0 +1,6 @@
+---
+title: Fix bug in Groups API when statistics are requested in an unauthenticated
+ API call
+merge_request: 32057
+author:
+type: fixed
diff --git a/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml
new file mode 100644
index 00000000000..0635a47d0b0
--- /dev/null
+++ b/changelogs/unreleased/22691-externalize-i18n-strings-from---app-views-shared-issuable-_nav-html.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n strings from ./app/views/shared/issuable/_nav.html.haml
+merge_request: 32165
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml
new file mode 100644
index 00000000000..be6007eab6d
--- /dev/null
+++ b/changelogs/unreleased/22691-externelize-i18n-strings-from---app-views-shared--aria-label.yml
@@ -0,0 +1,5 @@
+---
+title: Externalize i18n aria-label strings from ./app/views/shared/*
+merge_request: 32142
+author: Gilang Gumilar
+type: changed
diff --git a/changelogs/unreleased/24295-freeze-period-API-changes.yml b/changelogs/unreleased/24295-freeze-period-API-changes.yml
new file mode 100644
index 00000000000..31455bcc818
--- /dev/null
+++ b/changelogs/unreleased/24295-freeze-period-API-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Expose Freeze Periods in REST API
+merge_request: 29382
+author:
+type: added
diff --git a/changelogs/unreleased/ab-fix-not-null-inconsistency.yml b/changelogs/unreleased/ab-fix-not-null-inconsistency.yml
new file mode 100644
index 00000000000..6a874b8405d
--- /dev/null
+++ b/changelogs/unreleased/ab-fix-not-null-inconsistency.yml
@@ -0,0 +1,5 @@
+---
+title: Fix database schema inconsistency with not-null checks
+merge_request: 31930
+author:
+type: other
diff --git a/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb b/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb
new file mode 100644
index 00000000000..187ab0aa20b
--- /dev/null
+++ b/db/migrate/20200409105455_change_verification_checksum_field_type_in_package_file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class ChangeVerificationChecksumFieldTypeInPackageFile < ActiveRecord::Migration[6.0]
+ DOWNTIME = false
+
+ def up
+ # The use of this column is behind a feature flag that never got enabled,
+ # so it's safe to remove it in a normal migration
+ remove_column :packages_package_files, :verification_checksum, :string # rubocop:disable Migration/RemoveColumn
+ add_column :packages_package_files, :verification_checksum, :binary
+ end
+
+ def down
+ remove_column :packages_package_files, :verification_checksum, :binary
+ add_column :packages_package_files, :verification_checksum, :string
+ end
+end
diff --git a/db/migrate/20200409105456_add_checksum_index_to_package_file.rb b/db/migrate/20200409105456_add_checksum_index_to_package_file.rb
new file mode 100644
index 00000000000..07762109895
--- /dev/null
+++ b/db/migrate/20200409105456_add_checksum_index_to_package_file.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+class AddChecksumIndexToPackageFile < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_concurrent_index :packages_package_files, :verification_checksum, where: "(verification_checksum IS NOT NULL)", name: "packages_packages_verification_checksum_partial"
+ end
+
+ def down
+ remove_concurrent_index :packages_package_files, :verification_checksum
+ end
+end
diff --git a/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb b/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb
new file mode 100644
index 00000000000..a8bb91cf6cf
--- /dev/null
+++ b/db/migrate/20200513160930_fix_not_null_check_constraint_inconsistency.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class FixNotNullCheckConstraintInconsistency < ActiveRecord::Migration[6.0]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ table = :application_settings
+
+ %i(container_registry_vendor container_registry_version).each do |column|
+ change_column_null table, column, false
+ remove_not_null_constraint(table, column) if check_not_null_constraint_exists?(table, column)
+ end
+ end
+
+ def down
+ # No-op: for regular systems without the inconsistency, #up is a no-op, too
+ end
+end
diff --git a/db/structure.sql b/db/structure.sql
index 25f3e352d39..24fc55560d6 100644
--- a/db/structure.sql
+++ b/db/structure.sql
@@ -4668,9 +4668,9 @@ CREATE TABLE public.packages_package_files (
file_sha256 bytea,
verification_retry_at timestamp with time zone,
verified_at timestamp with time zone,
- verification_checksum character varying(255),
verification_failure character varying(255),
- verification_retry_count integer
+ verification_retry_count integer,
+ verification_checksum bytea
);
CREATE SEQUENCE public.packages_package_files_id_seq
@@ -13738,6 +13738,8 @@ COPY "schema_migrations" (version) FROM STDIN;
20200408175424
20200408212219
20200409085956
+20200409105455
+20200409105456
20200409211607
20200410104828
20200410232012
@@ -13817,6 +13819,7 @@ COPY "schema_migrations" (version) FROM STDIN;
20200511162115
20200512085150
20200512164334
+20200513160930
20200513234502
20200513235347
20200513235532
diff --git a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
index 46bc079971d..d7b32c8a92a 100644
--- a/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
+++ b/doc/administration/auth/how_to_configure_ldap_gitlab_ee/index.md
@@ -8,7 +8,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
## GitLab Enterprise Edition - LDAP features
-[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has a number of advantages when it comes to integrating with Active Directory (LDAP):
+[GitLab Enterprise Edition (EE)](https://about.gitlab.com/pricing/) has several advantages when it comes to integrating with Active Directory (LDAP):
- [Administrator Sync](../ldap-ee.md#administrator-sync): As an extension of group sync, you can automatically manage your global GitLab administrators. Specify a group CN for `admin_group` and all members of the LDAP group will be given administrator privileges.
- [Group Sync](#group-sync): This allows GitLab group membership to be automatically updated based on LDAP group members.
@@ -16,7 +16,7 @@ This article expands on [How to Configure LDAP with GitLab CE](../how_to_configu
- Daily user synchronization: Once a day, GitLab will run a synchronization to check and update GitLab users against LDAP. This process updates all user details automatically.
-On the following section, you'll find a description for each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
+In the following section, you'll find a description of each of these features. Read through [LDAP GitLab EE docs](../ldap-ee.md) for complementary information.
![GitLab OU Structure](img/admin_group.png)
@@ -28,7 +28,7 @@ Group syncing allows AD (LDAP) groups to be mapped to GitLab groups. This provid
#### Creating group links - example
-As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, it is important that users of this group are given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
+As an example, let's suppose we have a "UKGov" GitLab group, which deals with confidential government information. Therefore, users of this group must be given the correct permissions to projects contained within the group. Granular group permissions can be applied based on the AD group.
**UK Developers** of our "UKGov" group are given **"developer"** permissions.
diff --git a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
index c8d4ef767a4..33a64176357 100644
--- a/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
+++ b/doc/administration/troubleshooting/gitlab_rails_cheat_sheet.md
@@ -150,9 +150,9 @@ Project.update_all(visibility_level: 0)
#
projects = Project.where(pending_delete: true)
projects.each do |p|
- puts "Project name: #{p.id}"
+ puts "Project ID: #{p.id}"
puts "Project name: #{p.name}"
- puts "Repository path: #{p.repository.storage_path}"
+ puts "Repository path: #{p.repository.full_path}"
end
#
diff --git a/doc/api/merge_requests.md b/doc/api/merge_requests.md
index ffe73638dc1..3834bb6fee3 100644
--- a/doc/api/merge_requests.md
+++ b/doc/api/merge_requests.md
@@ -47,6 +47,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
@@ -65,6 +66,13 @@ Parameters:
| `wip` | string | no | Filter merge requests against their `wip` status. `yes` to return *only* WIP merge requests, `no` to return *non* WIP merge requests |
NOTE: **Note:**
+[Starting in GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890),
+listing merge requests may not proactively update the `merge_status` field
+(which also affects the `has_conflicts` field), as this can be an expensive
+operation. If you are interested in the value of these fields from this
+endpoint, set the `with_merge_status_recheck` parameter to `true` in the query.
+
+NOTE: **Note:**
[Starting in GitLab 12.8](https://gitlab.com/gitlab-org/gitlab/issues/29984),
when `async_merge_request_check_mergeability` feature flag is enabled, the
mergeability (`merge_status`) of each merge request will be checked
@@ -227,6 +235,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413) |
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
@@ -390,6 +399,7 @@ Parameters:
| `view` | string | no | If `simple`, returns the `iid`, URL, title, description, and basic state of merge request |
| `labels` | string | no | Return merge requests matching a comma separated list of labels. `None` lists all merge requests with no labels. `Any` lists all merge requests with at least one label. `No+Label` (Deprecated) lists all merge requests with no labels. Predefined names are case-insensitive. |
| `with_labels_details` | boolean | no | If `true`, response will return more details for each label in labels field: `:name`, `:color`, `:description`, `:description_html`, `:text_color`. Default is `false`. Introduced in [GitLab 12.7](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21413)|
+| `with_merge_status_recheck` | boolean | no | If `true`, this projection requests (but does not guarantee) that the `merge_status` field be recalculated asynchronously. Default is `false`. Introduced in [GitLab 13.0](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/31890) |
| `created_after` | datetime | no | Return merge requests created on or after the given time |
| `created_before` | datetime | no | Return merge requests created on or before the given time |
| `updated_after` | datetime | no | Return merge requests updated on or after the given time |
diff --git a/doc/development/code_review.md b/doc/development/code_review.md
index a83c0be218b..e49ad23d822 100644
--- a/doc/development/code_review.md
+++ b/doc/development/code_review.md
@@ -96,11 +96,16 @@ with [domain expertise](#domain-experts).
1. If your merge request includes documentation changes, it must be **approved
by a [Technical writer](https://about.gitlab.com/handbook/engineering/ux/technical-writing/#designated-technical-writers)**, based on
the appropriate [product category](https://about.gitlab.com/handbook/product/categories/).
+1. If your merge request includes Quality and non-Quality-related changes (*3*), it must be **approved
+ by a [Software Engineer in Test](https://about.gitlab.com/handbook/engineering/quality/#individual-contributors)**.
+1. If your merge request includes _only_ Quality-related changes (*3*), it must be **approved
+ by a [Quality maintainer](https://about.gitlab.com/handbook/engineering/projects/#gitlab_maintainers_qa)**.
- (*1*): Please note that specs other than JavaScript specs are considered backend code.
- (*2*): We encourage you to seek guidance from a database maintainer if your merge
request is potentially introducing expensive queries. It is most efficient to comment
on the line of code in question with the SQL queries so they can give their advice.
+- (*3*): Quality-related changes include all files within the `qa` directory.
#### Security requirements
@@ -320,6 +325,7 @@ Before taking the decision to merge:
- Consider warnings and errors from danger bot, code quality, and other reports.
Unless a strong case can be made for the violation, these should be resolved
before merging. A comment must to be posted if the MR is merged with any failed job.
+- If the MR contains both Quality and non-Quality-related changes, the MR should be merged by the relevant maintainer for user-facing changes (backend, frontend, or database) after the Quality related changes are approved by a Software Engineer in Test.
When ready to merge:
diff --git a/doc/development/geo/framework.md b/doc/development/geo/framework.md
index ad772166ff1..d72f7cc4cc1 100644
--- a/doc/development/geo/framework.md
+++ b/doc/development/geo/framework.md
@@ -324,7 +324,7 @@ Widgets should now be replicated by Geo!
def change
add_column :widgets, :verification_retry_at, :datetime_with_timezone
add_column :widgets, :verified_at, :datetime_with_timezone
- add_column :widgets, :verification_checksum, :string
+ add_column :widgets, :verification_checksum, :binary, using: 'verification_checksum::bytea'
add_column :widgets, :verification_failure, :string
add_column :widgets, :verification_retry_count, :integer
end
diff --git a/doc/development/testing_guide/best_practices.md b/doc/development/testing_guide/best_practices.md
index f610e9a1f77..f0137e542cc 100644
--- a/doc/development/testing_guide/best_practices.md
+++ b/doc/development/testing_guide/best_practices.md
@@ -23,10 +23,10 @@ and effective _as well as_ fast.
Here are some things to keep in mind regarding test performance:
-- `double` and `spy` are faster than `FactoryBot.build(...)`
+- `instance_double` and `spy` are faster than `FactoryBot.build(...)`
- `FactoryBot.build(...)` and `.build_stubbed` are faster than `.create`.
- Don't `create` an object when `build`, `build_stubbed`, `attributes_for`,
- `spy`, or `double` will do. Database persistence is slow!
+ `spy`, or `instance_double` will do. Database persistence is slow!
- Don't mark a feature as requiring JavaScript (through `:js` in RSpec) unless it's _actually_ required for the test
to be valid. Headless browser testing is slow!
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 74c03c8ee4e..09ad5f9afd7 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -19,7 +19,7 @@ as the hardware requirements that are needed to install and use GitLab.
- Scientific Linux (please use the CentOS packages and instructions)
- Oracle Linux (please use the CentOS packages and instructions)
-For the installations options, see [the main installation page](README.md).
+For the installation options, see [the main installation page](README.md).
### Unsupported Linux distributions and Unix-like operating systems
@@ -68,7 +68,7 @@ GitLab uses [webpack](https://webpack.js.org/) to compile frontend assets, which
version of Node.js 10.13.0.
You can check which version you are running with `node -v`. If you are running
-a version older than `v10.13.0`, you need to update to a newer version. You
+a version older than `v10.13.0`, you need to update it to a newer version. You
can find instructions to install from community maintained packages or compile
from source at the [Node.js website](https://nodejs.org/en/download/).
@@ -126,7 +126,7 @@ available when needed.
Our [Memory Team](https://about.gitlab.com/handbook/engineering/development/enablement/memory/) is actively working to reduce the memory requirement.
-NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need for those.
## Database
diff --git a/doc/user/group/epics/img/epic_view_v13.0.png b/doc/user/group/epics/img/epic_view_v13.0.png
index 84fb835d6c6..b25a91d318a 100644
--- a/doc/user/group/epics/img/epic_view_v13.0.png
+++ b/doc/user/group/epics/img/epic_view_v13.0.png
Binary files differ
diff --git a/doc/user/project/static_site_editor/index.md b/doc/user/project/static_site_editor/index.md
index b8841e221fe..e2e498b605a 100644
--- a/doc/user/project/static_site_editor/index.md
+++ b/doc/user/project/static_site_editor/index.md
@@ -8,6 +8,12 @@ description: "The static site editor enables users to edit content on static web
> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/28758) in GitLab 12.10.
> - WYSIWYG editor [introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/214559) in GitLab 13.0.
+DANGER: **Danger:**
+In GitLab 13.0, we [introduced breaking changes](https://gitlab.com/gitlab-org/gitlab/-/issues/213282)
+to the URL structure of the Static Site Editor. Follow the instructions in this
+[snippet](https://gitlab.com/gitlab-org/project-templates/static-site-editor-middleman/snippets/1976539)
+to update your project with the latest changes.
+
Static Site Editor enables users to edit content on static websites without
prior knowledge of the underlying templating language, site architecture, or
Git commands. A contributor to your project can quickly edit a Markdown page
diff --git a/doc/user/snippets.md b/doc/user/snippets.md
index 00014dc32ee..96c8dba11e5 100644
--- a/doc/user/snippets.md
+++ b/doc/user/snippets.md
@@ -148,7 +148,7 @@ snippet was created using the GitLab web interface the original line ending is W
> Introduced in GitLab 10.8.
Public snippets can not only be shared, but also embedded on any website. This
-allows to reuse a GitLab snippet in multiple places and any change to the source
+allows us to reuse a GitLab snippet in multiple places and any change to the source
is automatically reflected in the embedded snippet.
To embed a snippet, first make sure that:
@@ -172,6 +172,6 @@ Here's how an embedded snippet looks like:
<script src="https://gitlab.com/gitlab-org/gitlab-foss/snippets/1717978.js"></script>
-Embedded snippets are displayed with a header that shows the file name if defined,
+Embedded snippets are displayed with a header that shows the file name is defined,
the snippet size, a link to GitLab, and the actual snippet content. Actions in
the header allow users to see the snippet in raw format and download it.
diff --git a/lib/api/api.rb b/lib/api/api.rb
index c553630cbd3..86ddd6e57bc 100644
--- a/lib/api/api.rb
+++ b/lib/api/api.rb
@@ -141,6 +141,7 @@ module API
mount ::API::Events
mount ::API::Features
mount ::API::Files
+ mount ::API::FreezePeriods
mount ::API::GroupBoards
mount ::API::GroupClusters
mount ::API::GroupExport
diff --git a/lib/api/entities/freeze_period.rb b/lib/api/entities/freeze_period.rb
new file mode 100644
index 00000000000..9b5f08925db
--- /dev/null
+++ b/lib/api/entities/freeze_period.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module API
+ module Entities
+ class FreezePeriod < Grape::Entity
+ expose :id
+ expose :freeze_start, :freeze_end, :cron_timezone
+ expose :created_at, :updated_at
+ end
+ end
+end
diff --git a/lib/api/entities/merge_request_basic.rb b/lib/api/entities/merge_request_basic.rb
index 4610220e4f6..1a89a83a619 100644
--- a/lib/api/entities/merge_request_basic.rb
+++ b/lib/api/entities/merge_request_basic.rb
@@ -50,8 +50,10 @@ module API
# use `MergeRequest#mergeable?` instead (boolean).
# See https://gitlab.com/gitlab-org/gitlab-foss/issues/42344 for more
# information.
- expose :merge_status do |merge_request|
- merge_request.check_mergeability(async: true)
+ #
+ # For list endpoints, we skip the recheck by default, since it's expensive
+ expose :merge_status do |merge_request, options|
+ merge_request.check_mergeability(async: true) unless options[:skip_merge_status_recheck]
merge_request.public_merge_status
end
expose :diff_head_sha, as: :sha
diff --git a/lib/api/freeze_periods.rb b/lib/api/freeze_periods.rb
new file mode 100644
index 00000000000..9c7e5a5832d
--- /dev/null
+++ b/lib/api/freeze_periods.rb
@@ -0,0 +1,107 @@
+# frozen_string_literal: true
+
+module API
+ class FreezePeriods < Grape::API
+ include PaginationParams
+
+ before { authenticate! }
+
+ params do
+ requires :id, type: String, desc: 'The ID of a project'
+ end
+
+ resource :projects, requirements: API::NAMESPACE_OR_PROJECT_REQUIREMENTS do
+ desc 'Get project freeze periods' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ use :pagination
+ end
+
+ get ":id/freeze_periods" do
+ authorize! :read_freeze_period, user_project
+
+ freeze_periods = ::FreezePeriodsFinder.new(user_project, current_user).execute
+
+ present paginate(freeze_periods), with: Entities::FreezePeriod, current_user: current_user
+ end
+
+ desc 'Get a single freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_period_id, type: Integer, desc: 'The ID of a project freeze period'
+ end
+ get ":id/freeze_periods/:freeze_period_id" do
+ authorize! :read_freeze_period, user_project
+
+ present freeze_period, with: Entities::FreezePeriod, current_user: current_user
+ end
+
+ desc 'Create a new freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_start, type: String, desc: 'Freeze Period start'
+ requires :freeze_end, type: String, desc: 'Freeze Period end'
+ optional :cron_timezone, type: String, desc: 'Timezone'
+ end
+ post ':id/freeze_periods' do
+ authorize! :create_freeze_period, user_project
+
+ freeze_period_params = declared(params, include_parent_namespaces: false)
+
+ freeze_period = user_project.freeze_periods.create(freeze_period_params)
+
+ if freeze_period.persisted?
+ present freeze_period, with: Entities::FreezePeriod
+ else
+ render_validation_error!(freeze_period)
+ end
+ end
+
+ desc 'Update a freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ optional :freeze_start, type: String, desc: 'Freeze Period start'
+ optional :freeze_end, type: String, desc: 'Freeze Period end'
+ optional :cron_timezone, type: String, desc: 'Freeze Period Timezone'
+ end
+ put ':id/freeze_periods/:freeze_period_id' do
+ authorize! :update_freeze_period, user_project
+
+ freeze_period_params = declared(params, include_parent_namespaces: false, include_missing: false)
+
+ if freeze_period.update(freeze_period_params)
+ present freeze_period, with: Entities::FreezePeriod
+ else
+ render_validation_error!(freeze_period)
+ end
+ end
+
+ desc 'Delete a freeze period' do
+ detail 'This feature was introduced in GitLab 13.0.'
+ success Entities::FreezePeriod
+ end
+ params do
+ requires :freeze_period_id, type: Integer, desc: 'Freeze Period ID'
+ end
+ delete ':id/freeze_periods/:freeze_period_id' do
+ authorize! :destroy_freeze_period, user_project
+
+ destroy_conditionally!(freeze_period)
+ end
+ end
+
+ helpers do
+ def freeze_period
+ @freeze_period ||= user_project.freeze_periods.find(params[:freeze_period_id])
+ end
+ end
+ end
+end
diff --git a/lib/api/groups.rb b/lib/api/groups.rb
index dee134353b6..353c8b4b242 100644
--- a/lib/api/groups.rb
+++ b/lib/api/groups.rb
@@ -91,7 +91,7 @@ module API
options = {
with: Entities::Group,
current_user: current_user,
- statistics: params[:statistics] && current_user.admin?
+ statistics: params[:statistics] && current_user&.admin?
}
groups = groups.with_statistics if options[:statistics]
diff --git a/lib/api/helpers/merge_requests_helpers.rb b/lib/api/helpers/merge_requests_helpers.rb
index 73711a7e0ba..9dab2a88f0b 100644
--- a/lib/api/helpers/merge_requests_helpers.rb
+++ b/lib/api/helpers/merge_requests_helpers.rb
@@ -27,6 +27,7 @@ module API
coerce_with: Validations::Types::LabelsList.coerce,
desc: 'Comma-separated list of label names'
optional :with_labels_details, type: Boolean, desc: 'Return titles of labels and other details', default: false
+ optional :with_merge_status_recheck, type: Boolean, desc: 'Request that stale merge statuses be rechecked asynchronously', default: false
optional :created_after, type: DateTime, desc: 'Return merge requests created after the specified time'
optional :created_before, type: DateTime, desc: 'Return merge requests created before the specified time'
optional :updated_after, type: DateTime, desc: 'Return merge requests updated after the specified time'
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index 0284a055e2d..ff4ad85115b 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -93,6 +93,9 @@ module API
options[:with] = Entities::MergeRequestSimple
else
options[:issuable_metadata] = issuable_meta_data(merge_requests, 'MergeRequest', current_user)
+ if Feature.enabled?(:mr_list_api_skip_merge_status_recheck, default_enabled: true)
+ options[:skip_merge_status_recheck] = !declared_params[:with_merge_status_recheck]
+ end
end
options
diff --git a/lib/container_registry/client.rb b/lib/container_registry/client.rb
index fdd889f5416..118eb8e2d7c 100644
--- a/lib/container_registry/client.rb
+++ b/lib/container_registry/client.rb
@@ -13,6 +13,8 @@ module ContainerRegistry
DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE = 'application/vnd.docker.distribution.manifest.v2+json'
OCI_MANIFEST_V1_TYPE = 'application/vnd.oci.image.manifest.v1+json'
CONTAINER_IMAGE_V1_TYPE = 'application/vnd.docker.container.image.v1+json'
+ REGISTRY_VERSION_HEADER = 'gitlab-container-registry-version'
+ REGISTRY_FEATURES_HEADER = 'gitlab-container-registry-features'
ACCEPTED_TYPES = [DOCKER_DISTRIBUTION_MANIFEST_V2_TYPE, OCI_MANIFEST_V1_TYPE].freeze
@@ -24,6 +26,21 @@ module ContainerRegistry
@options = options
end
+ def registry_info
+ response = faraday.get("/v2/")
+
+ return {} unless response&.success?
+
+ version = response.headers[REGISTRY_VERSION_HEADER]
+ features = response.headers.fetch(REGISTRY_FEATURES_HEADER, '')
+
+ {
+ version: version,
+ features: features.split(',').map(&:strip),
+ vendor: version ? 'gitlab' : 'other'
+ }
+ end
+
def repository_tags(name)
response_body faraday.get("/v2/#{name}/tags/list")
end
diff --git a/lib/gitlab/experimentation.rb b/lib/gitlab/experimentation.rb
index 0097961eed4..3495b4a0b72 100644
--- a/lib/gitlab/experimentation.rb
+++ b/lib/gitlab/experimentation.rb
@@ -42,6 +42,9 @@ module Gitlab
},
buy_ci_minutes_version_a: {
tracking_category: 'Growth::Expansion::Experiment::BuyCiMinutesVersionA'
+ },
+ upgrade_link_in_user_menu_a: {
+ tracking_category: 'Growth::Expansion::Experiment::UpgradeLinkInUserMenuA'
}
}.freeze
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 605084f1ec2..a554dc0b667 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -57,11 +57,8 @@ module Gitlab
# Already a commit?
return commit_id if commit_id.is_a?(Gitlab::Git::Commit)
- # Some weird thing?
- return unless commit_id.is_a?(String)
-
# This saves us an RPC round trip.
- return if commit_id.include?(':')
+ return unless valid?(commit_id)
commit = find_commit(repo, commit_id)
@@ -431,6 +428,15 @@ module Gitlab
def fetch_body_from_gitaly
self.class.get_message(@repository, id)
end
+
+ def self.valid?(commit_id)
+ commit_id.is_a?(String) && !(
+ commit_id.start_with?('-') ||
+ commit_id.include?(':') ||
+ commit_id.include?("\x00") ||
+ commit_id.match?(/\s/)
+ )
+ end
end
end
end
diff --git a/lib/gitlab/usage_data.rb b/lib/gitlab/usage_data.rb
index 4ca89dbaa5e..2847a1f5d90 100644
--- a/lib/gitlab/usage_data.rb
+++ b/lib/gitlab/usage_data.rb
@@ -145,7 +145,8 @@ module Gitlab
services_usage,
usage_counters,
user_preferences_usage,
- ingress_modsecurity_usage
+ ingress_modsecurity_usage,
+ container_expiration_policies_usage
)
}
end
@@ -185,31 +186,8 @@ module Gitlab
web_ide_clientside_preview_enabled: alt_usage_data { Gitlab::CurrentSettings.web_ide_clientside_preview_enabled? },
ingress_modsecurity_enabled: Feature.enabled?(:ingress_modsecurity),
grafana_link_enabled: alt_usage_data { Gitlab::CurrentSettings.grafana_enabled? }
- }.merge(features_usage_data_container_expiration_policies)
- end
-
- # rubocop: disable CodeReuse/ActiveRecord
- def features_usage_data_container_expiration_policies
- results = {}
- start = ::Project.minimum(:id)
- finish = ::Project.maximum(:id)
-
- results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
- base = ::ContainerExpirationPolicy.active
- results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
-
- %i[keep_n cadence older_than].each do |option|
- ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
- results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
- end
- end
-
- results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
- results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
-
- results
+ }
end
- # rubocop: enable CodeReuse/ActiveRecord
# @return [Hash<Symbol, Integer>]
def usage_counters
@@ -316,6 +294,29 @@ module Gitlab
end
# rubocop: disable CodeReuse/ActiveRecord
+ def container_expiration_policies_usage
+ results = {}
+ start = ::Project.minimum(:id)
+ finish = ::Project.maximum(:id)
+
+ results[:projects_with_expiration_policy_disabled] = distinct_count(::ContainerExpirationPolicy.where(enabled: false), :project_id, start: start, finish: finish)
+ base = ::ContainerExpirationPolicy.active
+ results[:projects_with_expiration_policy_enabled] = distinct_count(base, :project_id, start: start, finish: finish)
+
+ %i[keep_n cadence older_than].each do |option|
+ ::ContainerExpirationPolicy.public_send("#{option}_options").keys.each do |value| # rubocop: disable GitlabSecurity/PublicSend
+ results["projects_with_expiration_policy_enabled_with_#{option}_set_to_#{value}".to_sym] = distinct_count(base.where(option => value), :project_id, start: start, finish: finish)
+ end
+ end
+
+ results[:projects_with_expiration_policy_enabled_with_keep_n_unset] = distinct_count(base.where(keep_n: nil), :project_id, start: start, finish: finish)
+ results[:projects_with_expiration_policy_enabled_with_older_than_unset] = distinct_count(base.where(older_than: nil), :project_id, start: start, finish: finish)
+
+ results
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
def services_usage
results = Service.available_services_names.without('jira').each_with_object({}) do |service_name, response|
response["projects_#{service_name}_active".to_sym] = count(Service.active.where(template: false, type: "#{service_name}_service".camelize))
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 1e233f7224c..017e651a563 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -2978,6 +2978,9 @@ msgstr ""
msgid "AutoDevOps|Auto DevOps documentation"
msgstr ""
+msgid "AutoDevOps|Dismiss Auto DevOps box"
+msgstr ""
+
msgid "AutoDevOps|Enable in settings"
msgstr ""
@@ -3506,6 +3509,9 @@ msgstr ""
msgid "BurndownChartLabel|Open issues"
msgstr ""
+msgid "Burnup chart"
+msgstr ""
+
msgid "Business"
msgstr ""
@@ -6471,6 +6477,9 @@ msgstr ""
msgid "CurrentUser|Start a Gold trial"
msgstr ""
+msgid "CurrentUser|Upgrade"
+msgstr ""
+
msgid "Custom CI configuration path"
msgstr ""
@@ -9443,9 +9452,21 @@ msgstr ""
msgid "Filter by %{issuable_type} that are currently opened."
msgstr ""
+msgid "Filter by %{page_context_word} that are currently opened."
+msgstr ""
+
msgid "Filter by commit message"
msgstr ""
+msgid "Filter by issues that are currently closed."
+msgstr ""
+
+msgid "Filter by merge requests that are currently closed and unmerged."
+msgstr ""
+
+msgid "Filter by merge requests that are currently merged."
+msgstr ""
+
msgid "Filter by milestone name"
msgstr ""
@@ -16276,6 +16297,9 @@ msgstr ""
msgid "Project cannot be shared with the group it is in or one of its ancestors."
msgstr ""
+msgid "Project clone URL"
+msgstr ""
+
msgid "Project configuration, including services"
msgstr ""
@@ -24603,6 +24627,9 @@ msgstr ""
msgid "You didn't renew your %{strong}%{plan_name}%{strong_close} subscription so it was downgraded to the GitLab Core Plan."
msgstr ""
+msgid "You do not have an active license"
+msgstr ""
+
msgid "You do not have any subscriptions yet"
msgstr ""
@@ -24651,6 +24678,9 @@ msgstr ""
msgid "You don’t have access to Value Stream Analytics for this group"
msgstr ""
+msgid "You have a license(s) that activates at a future date. Please see the License History table below."
+msgstr ""
+
msgid "You have been granted %{access_level} access to the %{source_link} %{source_type}."
msgstr ""
diff --git a/package.json b/package.json
index a0149a6c7df..483e4e1eddc 100644
--- a/package.json
+++ b/package.json
@@ -40,7 +40,7 @@
"@babel/preset-env": "^7.8.4",
"@gitlab/at.js": "1.5.5",
"@gitlab/svgs": "1.127.0",
- "@gitlab/ui": "14.5.0",
+ "@gitlab/ui": "14.10.0",
"@gitlab/visual-review-tools": "1.6.1",
"@rails/actioncable": "^6.0.2-2",
"@sentry/browser": "^5.10.2",
diff --git a/spec/controllers/repositories/git_http_controller_spec.rb b/spec/controllers/repositories/git_http_controller_spec.rb
index 034814bbfdc..1a2eee5d3a9 100644
--- a/spec/controllers/repositories/git_http_controller_spec.rb
+++ b/spec/controllers/repositories/git_http_controller_spec.rb
@@ -169,6 +169,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { project.owner }
end
+
it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
@@ -183,6 +184,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { personal_snippet.author }
end
+
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
@@ -197,6 +199,7 @@ describe Repositories::GitHttpController do
it_behaves_like 'info_refs behavior' do
let(:user) { project_snippet.author }
end
+
it_behaves_like 'git_upload_pack behavior', false
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccessSnippet }
diff --git a/spec/finders/freeze_periods_finder_spec.rb b/spec/finders/freeze_periods_finder_spec.rb
new file mode 100644
index 00000000000..4ff356b85b7
--- /dev/null
+++ b/spec/finders/freeze_periods_finder_spec.rb
@@ -0,0 +1,59 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe FreezePeriodsFinder do
+ subject(:finder) { described_class.new(project, user).execute }
+
+ let(:project) { create(:project, :private) }
+ let(:user) { create(:user) }
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
+
+ shared_examples_for 'returns nothing' do
+ specify do
+ is_expected.to be_empty
+ end
+ end
+
+ shared_examples_for 'returns freeze_periods ordered by created_at asc' do
+ it 'returns freeze_periods ordered by created_at' do
+ expect(subject.count).to eq(2)
+ expect(subject.pluck('id')).to eq([freeze_period_1.id, freeze_period_2.id])
+ end
+ end
+
+ context 'when user is a maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it_behaves_like 'returns freeze_periods ordered by created_at asc'
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it_behaves_like 'returns nothing'
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it_behaves_like 'returns nothing'
+ end
+
+ context 'when user is not a project member' do
+ it_behaves_like 'returns nothing'
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it_behaves_like 'returns nothing'
+ end
+ end
+end
diff --git a/spec/finders/projects/serverless/functions_finder_spec.rb b/spec/finders/projects/serverless/functions_finder_spec.rb
index 4e9f3d371ce..1f0e3cd2eda 100644
--- a/spec/finders/projects/serverless/functions_finder_spec.rb
+++ b/spec/finders/projects/serverless/functions_finder_spec.rb
@@ -48,6 +48,7 @@ describe Projects::Serverless::FunctionsFinder do
expect(function_finder.knative_installed).to be false
end
end
+
context 'when project level cluster is present and enabled' do
it_behaves_like 'before first deployment' do
let(:cluster) { create(:cluster, :project, :provided_by_gcp, enabled: true) }
diff --git a/spec/fixtures/api/schemas/public_api/v4/freeze_period.json b/spec/fixtures/api/schemas/public_api/v4/freeze_period.json
new file mode 100644
index 00000000000..b0187aee647
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/freeze_period.json
@@ -0,0 +1,20 @@
+{
+ "type": "object",
+ "required": [
+ "id",
+ "freeze_start",
+ "freeze_end",
+ "cron_timezone",
+ "created_at",
+ "updated_at"
+ ],
+ "properties": {
+ "id": { "type": "integer" },
+ "freeze_start": { "type": "string" },
+ "freeze_end": { "type": "string"},
+ "cron_timezone": { "type": "string" },
+ "created_at": { "type": "string" },
+ "updated_at": { "type": "string" }
+ },
+ "additionalProperties": false
+}
diff --git a/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json b/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json
new file mode 100644
index 00000000000..1e1c29a3b64
--- /dev/null
+++ b/spec/fixtures/api/schemas/public_api/v4/freeze_periods.json
@@ -0,0 +1,5 @@
+{
+ "type": "array",
+ "items": { "$ref": "freeze_period.json" }
+}
+
diff --git a/spec/frontend/header_spec.js b/spec/frontend/header_spec.js
index 0a74799283a..6d2d7976196 100644
--- a/spec/frontend/header_spec.js
+++ b/spec/frontend/header_spec.js
@@ -60,8 +60,8 @@ describe('Header', () => {
beforeEach(() => {
setFixtures(`
<li class="js-nav-user-dropdown">
- <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes
- </a>
+ <a class="js-buy-ci-minutes-link" data-track-event="click_buy_ci_minutes" data-track-label="free" data-track-property="user_dropdown">Buy CI minutes</a>
+ <a class="js-upgrade-plan-link" data-track-event="click_upgrade_link" data-track-label="free" data-track-property="user_dropdown">Upgrade</a>
</li>`);
trackingSpy = mockTracking('_category_', $('.js-nav-user-dropdown').element, jest.spyOn);
@@ -77,8 +77,16 @@ describe('Header', () => {
it('sends a tracking event when the dropdown is opened and contains Buy CI minutes link', () => {
$('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
- expect(trackingSpy).toHaveBeenCalledTimes(1);
- expect(trackingSpy).toHaveBeenCalledWith(undefined, 'show_buy_ci_minutes', {
+ expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_buy_ci_minutes', {
+ label: 'free',
+ property: 'user_dropdown',
+ });
+ });
+
+ it('sends a tracking event when the dropdown is opened and contains Upgrade link', () => {
+ $('.js-nav-user-dropdown').trigger('shown.bs.dropdown');
+
+ expect(trackingSpy).toHaveBeenCalledWith('some:page', 'show_upgrade_link', {
label: 'free',
property: 'user_dropdown',
});
diff --git a/spec/frontend/monitoring/components/dashboard_spec.js b/spec/frontend/monitoring/components/dashboard_spec.js
index 22bc3eef5f8..1fc02f0abe1 100644
--- a/spec/frontend/monitoring/components/dashboard_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_spec.js
@@ -19,6 +19,7 @@ import DashboardPanel from '~/monitoring/components/dashboard_panel.vue';
import { createStore } from '~/monitoring/stores';
import * as types from '~/monitoring/stores/mutation_types';
import {
+ setupAllDashboards,
setupStoreWithDashboard,
setMetricResult,
setupStoreWithData,
@@ -279,7 +280,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
- expect.stringContaining(`?${expectedSearch}`),
+ expect.stringContaining(`${expectedSearch}`),
);
});
});
@@ -302,7 +303,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
- expect.stringContaining(`?${expectedSearch}`),
+ expect.stringContaining(`${expectedSearch}`),
);
});
});
@@ -317,7 +318,7 @@ describe('Dashboard', () => {
expect(window.history.pushState).toHaveBeenCalledWith(
expect.anything(), // state
expect.any(String), // document title
- expect.not.stringContaining('?'), // no params
+ expect.not.stringMatching(/group|title|y_label/), // no panel params
);
});
});
@@ -359,6 +360,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper();
+ setupAllDashboards(store);
});
it('toggle star button is shown', () => {
@@ -380,10 +382,7 @@ describe('Dashboard', () => {
const getToggleTooltip = () => findToggleStar().element.parentElement.getAttribute('title');
beforeEach(() => {
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
+ setupAllDashboards(store);
jest.spyOn(store, 'dispatch');
});
@@ -400,7 +399,9 @@ describe('Dashboard', () => {
describe('when dashboard is not starred', () => {
beforeEach(() => {
- wrapper.setProps({ currentDashboard: dashboardGitResponse[0].path });
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[0].path,
+ });
return wrapper.vm.$nextTick();
});
@@ -415,7 +416,9 @@ describe('Dashboard', () => {
describe('when dashboard is starred', () => {
beforeEach(() => {
- wrapper.setProps({ currentDashboard: dashboardGitResponse[1].path });
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboardGitResponse[1].path,
+ });
return wrapper.vm.$nextTick();
});
@@ -551,7 +554,7 @@ describe('Dashboard', () => {
it('sets a link to the expanded panel', () => {
const searchQuery =
- '?group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
+ '?dashboard=config%2Fprometheus%2Fcommon_metrics.yml&group=System%20metrics%20(Kubernetes)&title=Memory%20Usage%20(Total)&y_label=Total%20Memory%20Used%20(GB)';
expect(findExpandedPanel().attributes('clipboard-text')).toEqual(
expect.stringContaining(searchQuery),
@@ -808,10 +811,7 @@ describe('Dashboard', () => {
beforeEach(() => {
createShallowWrapper({ hasMetrics: true });
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -820,10 +820,11 @@ describe('Dashboard', () => {
});
it('is present for a custom dashboard, and links to its edit_path', () => {
- const dashboard = dashboardGitResponse[1]; // non-default dashboard
- const currentDashboard = dashboard.path;
+ const dashboard = dashboardGitResponse[1];
+ store.commit(`monitoringDashboard/${types.SET_INITIAL_STATE}`, {
+ currentDashboard: dashboard.path,
+ });
- wrapper.setProps({ currentDashboard });
return wrapper.vm.$nextTick().then(() => {
expect(findEditLink().exists()).toBe(true);
expect(findEditLink().attributes('href')).toBe(dashboard.project_blob_path);
@@ -834,12 +835,7 @@ describe('Dashboard', () => {
describe('Dashboard dropdown', () => {
beforeEach(() => {
createMountedWrapper({ hasMetrics: true });
-
- wrapper.vm.$store.commit(
- `monitoringDashboard/${types.SET_ALL_DASHBOARDS}`,
- dashboardGitResponse,
- );
-
+ setupAllDashboards(store);
return wrapper.vm.$nextTick();
});
@@ -872,7 +868,7 @@ describe('Dashboard', () => {
});
describe('Clipboard text in panels', () => {
- const currentDashboard = 'TEST_DASHBOARD';
+ const currentDashboard = dashboardGitResponse[1].path;
const panelIndex = 1; // skip expanded panel
const getClipboardTextFirstPanel = () =>
@@ -882,37 +878,20 @@ describe('Dashboard', () => {
.props('clipboardText');
beforeEach(() => {
+ setupStoreWithData(store);
createShallowWrapper({ hasMetrics: true, currentDashboard });
- setupStoreWithData(wrapper.vm.$store);
-
return wrapper.vm.$nextTick();
});
it('contains a link to the dashboard', () => {
- expect(getClipboardTextFirstPanel()).toContain(`dashboard=${currentDashboard}`);
+ const dashboardParam = `dashboard=${encodeURIComponent(currentDashboard)}`;
+
+ expect(getClipboardTextFirstPanel()).toContain(dashboardParam);
expect(getClipboardTextFirstPanel()).toContain(`group=`);
expect(getClipboardTextFirstPanel()).toContain(`title=`);
expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
});
-
- it('strips the undefined parameter', () => {
- wrapper.setProps({ currentDashboard: undefined });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
- expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
- });
- });
-
- it('null parameter is stripped', () => {
- wrapper.setProps({ currentDashboard: null });
-
- return wrapper.vm.$nextTick(() => {
- expect(getClipboardTextFirstPanel()).not.toContain(`dashboard=`);
- expect(getClipboardTextFirstPanel()).toContain(`y_label=`);
- });
- });
});
describe('add custom metrics', () => {
diff --git a/spec/frontend/monitoring/components/dashboard_template_spec.js b/spec/frontend/monitoring/components/dashboard_template_spec.js
index 0257515f18e..cc0ac348b11 100644
--- a/spec/frontend/monitoring/components/dashboard_template_spec.js
+++ b/spec/frontend/monitoring/components/dashboard_template_spec.js
@@ -3,6 +3,7 @@ import MockAdapter from 'axios-mock-adapter';
import axios from '~/lib/utils/axios_utils';
import Dashboard from '~/monitoring/components/dashboard.vue';
import { createStore } from '~/monitoring/stores';
+import { setupAllDashboards } from '../store_utils';
import { propsData } from '../mock_data';
jest.mock('~/lib/utils/url_utility');
@@ -15,6 +16,8 @@ describe('Dashboard template', () => {
beforeEach(() => {
store = createStore();
mock = new MockAdapter(axios);
+
+ setupAllDashboards(store);
});
afterEach(() => {
diff --git a/spec/frontend/monitoring/store_utils.js b/spec/frontend/monitoring/store_utils.js
index b3f87fb7abf..3cfd3baaee7 100644
--- a/spec/frontend/monitoring/store_utils.js
+++ b/spec/frontend/monitoring/store_utils.js
@@ -1,5 +1,5 @@
import * as types from '~/monitoring/stores/mutation_types';
-import { metricsResult, environmentData } from './mock_data';
+import { metricsResult, environmentData, dashboardGitResponse } from './mock_data';
import { metricsDashboardPayload } from './fixture_data';
export const setMetricResult = ({ $store, result, group = 0, panel = 0, metric = 0 }) => {
@@ -16,11 +16,19 @@ const setEnvironmentData = $store => {
$store.commit(`monitoringDashboard/${types.RECEIVE_ENVIRONMENTS_DATA_SUCCESS}`, environmentData);
};
+export const setupAllDashboards = $store => {
+ $store.commit(`monitoringDashboard/${types.SET_ALL_DASHBOARDS}`, dashboardGitResponse);
+};
+
export const setupStoreWithDashboard = $store => {
$store.commit(
`monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
metricsDashboardPayload,
);
+ $store.commit(
+ `monitoringDashboard/${types.RECEIVE_METRICS_DASHBOARD_SUCCESS}`,
+ metricsDashboardPayload,
+ );
};
export const setupStoreWithVariable = $store => {
@@ -30,6 +38,7 @@ export const setupStoreWithVariable = $store => {
};
export const setupStoreWithData = $store => {
+ setupAllDashboards($store);
setupStoreWithDashboard($store);
setMetricResult({ $store, result: [], panel: 0 });
diff --git a/spec/frontend/notes/components/note_header_spec.js b/spec/frontend/notes/components/note_header_spec.js
index 8cb78720c7e..9b9ce0a8ed0 100644
--- a/spec/frontend/notes/components/note_header_spec.js
+++ b/spec/frontend/notes/components/note_header_spec.js
@@ -2,7 +2,6 @@ import { shallowMount, createLocalVue } from '@vue/test-utils';
import { nextTick } from 'vue';
import Vuex from 'vuex';
import NoteHeader from '~/notes/components/note_header.vue';
-import GitlabTeamMemberBadge from '~/vue_shared/components/user_avatar/badges/gitlab_team_member_badge.vue';
const localVue = createLocalVue();
localVue.use(Vuex);
@@ -141,20 +140,6 @@ describe('NoteHeader component', () => {
});
});
- test.each`
- props | expected | message1 | message2
- ${{ author: { ...author, is_gitlab_employee: true } }} | ${true} | ${'renders'} | ${'true'}
- ${{ author: { ...author, is_gitlab_employee: false } }} | ${false} | ${"doesn't render"} | ${'false'}
- ${{ author }} | ${false} | ${"doesn't render"} | ${'undefined'}
- `(
- '$message1 GitLab team member badge when `is_gitlab_employee` is $message2',
- ({ props, expected }) => {
- createComponent(props);
-
- expect(wrapper.find(GitlabTeamMemberBadge).exists()).toBe(expected);
- },
- );
-
describe('loading spinner', () => {
it('shows spinner when showSpinner is true', () => {
createComponent();
diff --git a/spec/frontend/releases/stores/modules/detail/mutations_spec.js b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
index 155390c2cee..f3f7ca797b4 100644
--- a/spec/frontend/releases/stores/modules/detail/mutations_spec.js
+++ b/spec/frontend/releases/stores/modules/detail/mutations_spec.js
@@ -20,7 +20,7 @@ describe('Release detail mutations', () => {
release = convertObjectPropsToCamelCase(originalRelease);
});
- describe(types.REQUEST_RELEASE, () => {
+ describe(`${types.REQUEST_RELEASE}`, () => {
it('set state.isFetchingRelease to true', () => {
mutations[types.REQUEST_RELEASE](state);
@@ -28,7 +28,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_RELEASE_SUCCESS, () => {
+ describe(`${types.RECEIVE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_RELEASE_SUCCESS](state, release);
@@ -42,7 +42,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_RELEASE_ERROR, () => {
+ describe(`${types.RECEIVE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_RELEASE_ERROR](state, error);
@@ -55,7 +55,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_RELEASE_TITLE, () => {
+ describe(`${types.UPDATE_RELEASE_TITLE}`, () => {
it("updates the release's title", () => {
state.release = release;
const newTitle = 'The new release title';
@@ -65,7 +65,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_RELEASE_NOTES, () => {
+ describe(`${types.UPDATE_RELEASE_NOTES}`, () => {
it("updates the release's notes", () => {
state.release = release;
const newNotes = 'The new release notes';
@@ -75,7 +75,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.REQUEST_UPDATE_RELEASE, () => {
+ describe(`${types.REQUEST_UPDATE_RELEASE}`, () => {
it('set state.isUpdatingRelease to true', () => {
mutations[types.REQUEST_UPDATE_RELEASE](state);
@@ -83,7 +83,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_UPDATE_RELEASE_SUCCESS, () => {
+ describe(`${types.RECEIVE_UPDATE_RELEASE_SUCCESS}`, () => {
it('handles a successful response from the server', () => {
mutations[types.RECEIVE_UPDATE_RELEASE_SUCCESS](state, release);
@@ -93,7 +93,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.RECEIVE_UPDATE_RELEASE_ERROR, () => {
+ describe(`${types.RECEIVE_UPDATE_RELEASE_ERROR}`, () => {
it('handles an unsuccessful response from the server', () => {
const error = { message: 'An error occurred!' };
mutations[types.RECEIVE_UPDATE_RELEASE_ERROR](state, error);
@@ -104,7 +104,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.ADD_EMPTY_ASSET_LINK, () => {
+ describe(`${types.ADD_EMPTY_ASSET_LINK}`, () => {
it('adds a new, empty link object to the release', () => {
state.release = release;
@@ -123,7 +123,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_ASSET_LINK_URL, () => {
+ describe(`${types.UPDATE_ASSET_LINK_URL}`, () => {
it('updates an asset link with a new URL', () => {
state.release = release;
@@ -138,7 +138,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.UPDATE_ASSET_LINK_NAME, () => {
+ describe(`${types.UPDATE_ASSET_LINK_NAME}`, () => {
it('updates an asset link with a new name', () => {
state.release = release;
@@ -153,7 +153,7 @@ describe('Release detail mutations', () => {
});
});
- describe(types.REMOVE_ASSET_LINK, () => {
+ describe(`${types.REMOVE_ASSET_LINK}`, () => {
it('removes an asset link from the release', () => {
state.release = release;
diff --git a/spec/lib/container_registry/client_spec.rb b/spec/lib/container_registry/client_spec.rb
index 0aad6568793..18bcff65f41 100644
--- a/spec/lib/container_registry/client_spec.rb
+++ b/spec/lib/container_registry/client_spec.rb
@@ -85,7 +85,7 @@ describe ContainerRegistry::Client do
it 'follows 307 redirect for GET /v2/:name/blobs/:digest' do
stub_request(:get, "http://container-registry/v2/group/test/blobs/sha256:0123456789012345")
.with(headers: blob_headers)
- .to_return(status: 307, body: "", headers: { Location: 'http://redirected' })
+ .to_return(status: 307, body: '', headers: { Location: 'http://redirected' })
# We should probably use hash_excluding here, but that requires an update to WebMock:
# https://github.com/bblimke/webmock/blob/master/lib/webmock/matchers/hash_excluding_matcher.rb
stub_request(:get, "http://redirected/")
@@ -238,4 +238,54 @@ describe ContainerRegistry::Client do
it { is_expected.to be_falsey }
end
end
+
+ def stub_registry_info(headers: {}, status: 200)
+ stub_request(:get, 'http://container-registry/v2/')
+ .to_return(status: status, body: "", headers: headers)
+ end
+
+ describe '#registry_info' do
+ subject { client.registry_info }
+
+ context 'when the check is successful' do
+ context 'when using the GitLab container registry' do
+ before do
+ stub_registry_info(headers: {
+ 'GitLab-Container-Registry-Version' => '2.9.1-gitlab',
+ 'GitLab-Container-Registry-Features' => 'a,b,c'
+ })
+ end
+
+ it 'identifies the vendor as "gitlab"' do
+ expect(subject).to include(vendor: 'gitlab')
+ end
+
+ it 'identifies version and features' do
+ expect(subject).to include(version: '2.9.1-gitlab', features: %w[a b c])
+ end
+ end
+
+ context 'when using a third-party container registry' do
+ before do
+ stub_registry_info
+ end
+
+ it 'identifies the vendor as "other"' do
+ expect(subject).to include(vendor: 'other')
+ end
+
+ it 'does not identify version or features' do
+ expect(subject).to include(version: nil, features: [])
+ end
+ end
+ end
+
+ context 'when the check is not successful' do
+ it 'does not identify vendor, version or features' do
+ stub_registry_info(status: 500)
+
+ expect(subject).to eq({})
+ end
+ end
+ end
end
diff --git a/spec/lib/gitlab/git/commit_spec.rb b/spec/lib/gitlab/git/commit_spec.rb
index c2fc228d34a..edd367673fb 100644
--- a/spec/lib/gitlab/git/commit_spec.rb
+++ b/spec/lib/gitlab/git/commit_spec.rb
@@ -161,6 +161,26 @@ describe Gitlab::Git::Commit, :seed_helper do
expect(described_class.find(repository, "+123_4532530XYZ")).to be_nil
end
+ it "returns nil for id started with dash" do
+ expect(described_class.find(repository, "-HEAD")).to be_nil
+ end
+
+ it "returns nil for id containing colon" do
+ expect(described_class.find(repository, "HEAD:")).to be_nil
+ end
+
+ it "returns nil for id containing space" do
+ expect(described_class.find(repository, "HE AD")).to be_nil
+ end
+
+ it "returns nil for id containing tab" do
+ expect(described_class.find(repository, "HE\tAD")).to be_nil
+ end
+
+ it "returns nil for id containing NULL" do
+ expect(described_class.find(repository, "HE\x00AD")).to be_nil
+ end
+
context 'with broken repo' do
let(:repository) { Gitlab::Git::Repository.new('default', TEST_BROKEN_REPO_PATH, '', 'group/project') }
diff --git a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
index cff489e0f3b..afbc48e9ca2 100644
--- a/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
+++ b/spec/lib/gitlab/hook_data/issuable_builder_spec.rb
@@ -12,6 +12,7 @@ describe Gitlab::HookData::IssuableBuilder do
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
+
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
diff --git a/spec/lib/gitlab/legacy_github_import/importer_spec.rb b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
index af0bffa91a5..8cc3fd8efbd 100644
--- a/spec/lib/gitlab/legacy_github_import/importer_spec.rb
+++ b/spec/lib/gitlab/legacy_github_import/importer_spec.rb
@@ -294,6 +294,7 @@ describe Gitlab::LegacyGithubImport::Importer do
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute' do
let(:expected_not_called) { [:import_releases, [:import_comments, :pull_requests]] }
end
+
it_behaves_like 'Gitlab::LegacyGithubImport::Importer#execute an error occurs'
it_behaves_like 'Gitlab::LegacyGithubImport unit-testing'
diff --git a/spec/lib/gitlab/usage_data_spec.rb b/spec/lib/gitlab/usage_data_spec.rb
index cd4b14979a4..6a905c49ea9 100644
--- a/spec/lib/gitlab/usage_data_spec.rb
+++ b/spec/lib/gitlab/usage_data_spec.rb
@@ -99,6 +99,46 @@ describe Gitlab::UsageData, :aggregate_failures do
)
end
+ context 'with existing container expiration policies' do
+ let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
+ let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
+
+ %i[keep_n cadence older_than].each do |attribute|
+ ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
+ let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
+ end
+ end
+
+ let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
+ let(:active_policies) { ::ContainerExpirationPolicy.active }
+
+ subject { described_class.data[:counts] }
+
+ it 'gathers usage data' do
+ expect(subject[:projects_with_expiration_policy_enabled]).to eq 20
+ expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 14
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 16
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
+
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 5
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
+ expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
+ end
+ end
+
it 'works when queries time out' do
allow_any_instance_of(ActiveRecord::Relation)
.to receive(:count).and_raise(ActiveRecord::StatementInvalid.new(''))
@@ -192,43 +232,6 @@ describe Gitlab::UsageData, :aggregate_failures do
expect(subject[:grafana_link_enabled]).to eq(false)
end
end
-
- context 'with existing container expiration policies' do
- let_it_be(:disabled) { create(:container_expiration_policy, enabled: false) }
- let_it_be(:enabled) { create(:container_expiration_policy, enabled: true) }
- %i[keep_n cadence older_than].each do |attribute|
- ContainerExpirationPolicy.send("#{attribute}_options").keys.each do |value|
- let_it_be("container_expiration_policy_with_#{attribute}_set_to_#{value}") { create(:container_expiration_policy, attribute => value) }
- end
- end
-
- let(:inactive_policies) { ::ContainerExpirationPolicy.where(enabled: false) }
- let(:active_policies) { ::ContainerExpirationPolicy.active }
-
- it 'gathers usage data' do
- expect(subject[:projects_with_expiration_policy_enabled]).to eq 16
- expect(subject[:projects_with_expiration_policy_disabled]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_unset]).to eq 10
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_1]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_5]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_10]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_25]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_keep_n_set_to_50]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_unset]).to eq 12
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_30d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_older_than_set_to_90d]).to eq 1
-
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1d]).to eq 12
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_7d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_14d]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_1month]).to eq 1
- expect(subject[:projects_with_expiration_policy_enabled_with_cadence_set_to_3month]).to eq 1
- end
- end
end
describe '#components_usage_data' do
diff --git a/spec/models/ci/freeze_period_spec.rb b/spec/models/ci/freeze_period_spec.rb
index 0e3dd658b98..f7f840c6696 100644
--- a/spec/models/ci/freeze_period_spec.rb
+++ b/spec/models/ci/freeze_period_spec.rb
@@ -5,6 +5,8 @@ require 'spec_helper'
RSpec.describe Ci::FreezePeriod, type: :model do
subject { build(:ci_freeze_period) }
+ let(:invalid_cron) { '0 0 0 * *' }
+
it { is_expected.to belong_to(:project) }
it { is_expected.to respond_to(:freeze_start) }
@@ -13,13 +15,19 @@ RSpec.describe Ci::FreezePeriod, type: :model do
describe 'cron validations' do
it 'allows valid cron patterns' do
- freeze_period = build(:ci_freeze_period, freeze_start: '0 23 * * 5')
+ freeze_period = build(:ci_freeze_period)
expect(freeze_period).to be_valid
end
- it 'does not allow invalid cron patterns' do
- freeze_period = build(:ci_freeze_period, freeze_start: '0 0 0 * *')
+ it 'does not allow invalid cron patterns on freeze_start' do
+ freeze_period = build(:ci_freeze_period, freeze_start: invalid_cron)
+
+ expect(freeze_period).not_to be_valid
+ end
+
+ it 'does not allow invalid cron patterns on freeze_end' do
+ freeze_period = build(:ci_freeze_period, freeze_end: invalid_cron)
expect(freeze_period).not_to be_valid
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index 5e0c31c3293..49294256351 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -300,6 +300,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -309,6 +310,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
end
@@ -320,6 +322,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to assignee and author', true
end
@@ -329,6 +332,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:member, :admin) }
end
+
include_examples 'visible to assignee and author', true
end
@@ -429,6 +433,7 @@ describe Event do
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
@@ -440,6 +445,7 @@ describe Event do
end
# Normally, we'd expect the author of a comment to be able to view it.
# However, this doesn't seem to be the case for comments on snippets.
+
include_examples 'visible to author', false
end
end
@@ -450,6 +456,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_all }
end
+
include_examples 'visible to author', true
context 'on internal snippet' do
@@ -466,6 +473,7 @@ describe Event do
include_examples 'visibility examples' do
let(:visibility) { visible_to_none_except(:admin) }
end
+
include_examples 'visible to author', true
end
end
diff --git a/spec/models/milestone_spec.rb b/spec/models/milestone_spec.rb
index 9aaaf536351..e51108947a7 100644
--- a/spec/models/milestone_spec.rb
+++ b/spec/models/milestone_spec.rb
@@ -480,4 +480,22 @@ describe Milestone do
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
+
+ describe '#parent' do
+ context 'with group' do
+ it 'returns the expected parent' do
+ group = create(:group)
+
+ expect(build(:milestone, group: group).parent).to eq(group)
+ end
+ end
+
+ context 'with project' do
+ it 'returns the expected parent' do
+ project = create(:project)
+
+ expect(build(:milestone, project: project).parent).to eq(project)
+ end
+ end
+ end
end
diff --git a/spec/requests/api/freeze_periods_spec.rb b/spec/requests/api/freeze_periods_spec.rb
new file mode 100644
index 00000000000..0b7828ebedf
--- /dev/null
+++ b/spec/requests/api/freeze_periods_spec.rb
@@ -0,0 +1,475 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe API::FreezePeriods do
+ let_it_be(:project) { create(:project, :repository, :private) }
+ let_it_be(:user) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let(:api_user) { user }
+ let(:invalid_cron) { '0 0 0 * *' }
+ let(:last_freeze_period) { project.freeze_periods.last }
+
+ describe 'GET /projects/:id/freeze_periods' do
+ context 'when the user is the admin' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'when there are two freeze_periods' do
+ let!(:freeze_period_1) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+ let!(:freeze_period_2) { create(:ci_freeze_period, project: project, created_at: 1.day.ago) }
+
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns freeze_periods ordered by created_at ascending' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(json_response.count).to eq(2)
+ expect(freeze_period_ids).to eq([freeze_period_1.id, freeze_period_2.id])
+ end
+
+ it 'matches response schema' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to match_response_schema('public_api/v4/freeze_periods')
+ end
+ end
+
+ context 'when there are no freeze_periods' do
+ it 'returns 200 HTTP status' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns an empty response' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(json_response).to be_empty
+ end
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ let!(:freeze_period) do
+ create(:ci_freeze_period, project: project)
+ end
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'GET /projects/:id/freeze_periods/:freeze_period_id' do
+ context 'when there is a freeze period' do
+ let!(:freeze_period) do
+ create(:ci_freeze_period, project: project)
+ end
+
+ context 'when the user is the admin' do
+ let!(:freeze_period) { create(:ci_freeze_period, project: project, created_at: 2.days.ago) }
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", admin)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when the user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'responds 200 OK' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'returns a freeze period' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(json_response).to include(
+ 'id' => freeze_period.id,
+ 'freeze_start' => freeze_period.freeze_start,
+ 'freeze_end' => freeze_period.freeze_end,
+ 'cron_timezone' => freeze_period.cron_timezone)
+ end
+
+ it 'matches response schema' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'when user is a guest' do
+ before do
+ project.add_guest(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ context 'when freeze_period exists' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when freeze_period does not exist' do
+ it 'responds 403 Forbidden' do
+ get api("/projects/#{project.id}/freeze_periods/0", user)
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+ end
+ end
+
+ describe 'POST /projects/:id/freeze_periods' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ freeze_end: '0 7 * * 1',
+ cron_timezone: 'UTC'
+ }
+ end
+
+ subject { post api("/projects/#{project.id}/freeze_periods", api_user), params: params }
+
+ context 'when the user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'with valid params' do
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:created)
+ end
+
+ it 'creates a new freeze period' do
+ expect do
+ subject
+ end.to change { Ci::FreezePeriod.count }.by(1)
+
+ expect(last_freeze_period.freeze_start).to eq('0 23 * * 5')
+ expect(last_freeze_period.freeze_end).to eq('0 7 * * 1')
+ expect(last_freeze_period.cron_timezone).to eq('UTC')
+ end
+
+ it 'matches response schema' do
+ subject
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'with incomplete params' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ cron_timezone: 'UTC'
+ }
+ end
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['error']).to eq("freeze_end is missing")
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) do
+ {
+ freeze_start: '0 23 * * 5',
+ freeze_end: invalid_cron,
+ cron_timezone: 'UTC'
+ }
+ end
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['freeze_end']).to eq([" is invalid syntax"])
+ end
+ end
+ end
+
+ context 'when user is a developer' do
+ before do
+ project.add_developer(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'PUT /projects/:id/freeze_periods/:freeze_period_id' do
+ let(:params) { { freeze_start: '0 22 * * 5', freeze_end: '5 4 * * sun' } }
+ let!(:freeze_period) { create :ci_freeze_period, project: project }
+
+ subject { put api("/projects/#{project.id}/freeze_periods/#{freeze_period.id}", api_user), params: params }
+
+ context 'when user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ context 'with valid params' do
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:ok)
+ end
+
+ it 'performs the update' do
+ subject
+
+ freeze_period.reload
+
+ expect(freeze_period.freeze_start).to eq(params[:freeze_start])
+ expect(freeze_period.freeze_end).to eq(params[:freeze_end])
+ end
+
+ it 'matches response schema' do
+ subject
+
+ expect(response).to match_response_schema('public_api/v4/freeze_period')
+ end
+ end
+
+ context 'with invalid params' do
+ let(:params) { { freeze_start: invalid_cron } }
+
+ it 'responds 400 Bad Request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:bad_request)
+ expect(json_response['message']['freeze_start']).to eq([" is invalid syntax"])
+ end
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ describe 'DELETE /projects/:id/freeze_periods/:freeze_period_id' do
+ let!(:freeze_period) { create :ci_freeze_period, project: project }
+ let(:freeze_period_id) { freeze_period.id }
+
+ subject { delete api("/projects/#{project.id}/freeze_periods/#{freeze_period_id}", api_user) }
+
+ context 'when user is the admin' do
+ let(:api_user) { admin }
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+ end
+
+ context 'when user is the maintainer' do
+ before do
+ project.add_maintainer(user)
+ end
+
+ it 'accepts the request' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:no_content)
+ end
+
+ it 'destroys the freeze period' do
+ expect do
+ subject
+ end.to change { Ci::FreezePeriod.count }.by(-1)
+ end
+
+ context 'when it is a non-existing freeze period id' do
+ let(:freeze_period_id) { 0 }
+
+ it '404' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+ end
+ end
+
+ context 'when user is a reporter' do
+ before do
+ project.add_reporter(user)
+ end
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+
+ context 'when user is not a project member' do
+ it 'responds 404 Not Found' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:not_found)
+ end
+
+ context 'when project is public' do
+ let(:project) { create(:project, :public) }
+
+ it 'responds 403 Forbidden' do
+ subject
+
+ expect(response).to have_gitlab_http_status(:forbidden)
+ end
+ end
+ end
+ end
+
+ def freeze_period_ids
+ json_response.map do |freeze_period_hash|
+ freeze_period_hash.fetch('id')&.to_i
+ end
+ end
+end
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index 2ddb16d0532..82d700c2326 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -6,15 +6,15 @@ describe API::Groups do
include GroupAPIHelpers
include UploadHelpers
- let(:user1) { create(:user, can_create_group: false) }
- let(:user2) { create(:user) }
- let(:user3) { create(:user) }
- let(:admin) { create(:admin) }
- let!(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
- let!(:group2) { create(:group, :private) }
- let!(:project1) { create(:project, namespace: group1) }
- let!(:project2) { create(:project, namespace: group2) }
- let!(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
+ let_it_be(:user1) { create(:user, can_create_group: false) }
+ let_it_be(:user2) { create(:user) }
+ let_it_be(:user3) { create(:user) }
+ let_it_be(:admin) { create(:admin) }
+ let_it_be(:group1) { create(:group, avatar: File.open(uploaded_image_temp_path)) }
+ let_it_be(:group2) { create(:group, :private) }
+ let_it_be(:project1) { create(:project, namespace: group1) }
+ let_it_be(:project2) { create(:project, namespace: group2) }
+ let_it_be(:project3) { create(:project, namespace: group1, path: 'test', visibility_level: Gitlab::VisibilityLevel::PRIVATE) }
before do
group1.add_owner(user1)
@@ -90,6 +90,17 @@ describe API::Groups do
get api("/groups", admin)
end.not_to exceed_query_limit(control)
end
+
+ context 'when statistics are requested' do
+ it 'does not include statistics' do
+ get api("/groups"), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
end
context "when authenticated as user" do
@@ -1113,6 +1124,17 @@ describe API::Groups do
expect(response).to have_gitlab_http_status(:not_found)
end
+
+ context 'when statistics are requested' do
+ it 'does not include statistics' do
+ get api("/groups/#{group1.id}/subgroups"), params: { statistics: true }
+
+ expect(response).to have_gitlab_http_status(:ok)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.first).not_to include 'statistics'
+ end
+ end
end
context 'when authenticated as user' do
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index d3999d1ef87..8ab8207bc49 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -66,17 +66,36 @@ describe API::MergeRequests do
end
context 'when merge request is unchecked' do
+ let(:check_service_class) { MergeRequests::MergeabilityCheckService }
+ let(:mr_entity) { json_response.find { |mr| mr['id'] == merge_request.id } }
+
before do
merge_request.mark_as_unchecked!
end
- it 'checks mergeability asynchronously' do
- expect_next_instance_of(MergeRequests::MergeabilityCheckService) do |service|
- expect(service).not_to receive(:execute)
- expect(service).to receive(:async_execute)
+ context 'with merge status recheck projection' do
+ it 'checks mergeability asynchronously' do
+ expect_next_instance_of(check_service_class) do |service|
+ expect(service).not_to receive(:execute)
+ expect(service).to receive(:async_execute).and_call_original
+ end
+
+ get(api(endpoint_path, user), params: { with_merge_status_recheck: true })
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('checking')
end
+ end
- get api(endpoint_path, user)
+ context 'without merge status recheck projection' do
+ it 'does not enqueue a merge status recheck' do
+ expect(check_service_class).not_to receive(:new)
+
+ get api(endpoint_path, user)
+
+ expect_successful_response_with_paginated_array
+ expect(mr_entity['merge_status']).to eq('unchecked')
+ end
end
end
diff --git a/spec/services/projects/create_service_spec.rb b/spec/services/projects/create_service_spec.rb
index 5b0e32dcd67..e542f1e9108 100644
--- a/spec/services/projects/create_service_spec.rb
+++ b/spec/services/projects/create_service_spec.rb
@@ -510,6 +510,83 @@ describe Projects::CreateService, '#execute' do
end
end
+ context 'with specialized_project_authorization_workers' do
+ let_it_be(:other_user) { create(:user) }
+ let_it_be(:group) { create(:group) }
+
+ let(:opts) do
+ {
+ name: 'GitLab',
+ namespace_id: group.id
+ }
+ end
+
+ before do
+ group.add_maintainer(user)
+ group.add_developer(other_user)
+ end
+
+ it 'updates authorization for current_user' do
+ expect(Users::RefreshAuthorizedProjectsService).to(
+ receive(:new).with(user).and_call_original
+ )
+
+ project = create_project(user, opts)
+
+ expect(
+ Ability.allowed?(user, :read_project, project)
+ ).to be_truthy
+ end
+
+ it 'schedules authorization update for users with access to group' do
+ expect(AuthorizedProjectsWorker).not_to(
+ receive(:bulk_perform_async)
+ )
+ expect(AuthorizedProjectUpdate::ProjectCreateWorker).to(
+ receive(:perform_async).and_call_original
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ receive(:bulk_perform_in)
+ .with(1.hour, array_including([user.id], [other_user.id]))
+ .and_call_original
+ )
+
+ create_project(user, opts)
+ end
+
+ context 'when feature is disabled' do
+ before do
+ stub_feature_flags(specialized_project_authorization_workers: false)
+ end
+
+ it 'updates authorization for current_user' do
+ expect(Users::RefreshAuthorizedProjectsService).to(
+ receive(:new).with(user).and_call_original
+ )
+
+ project = create_project(user, opts)
+
+ expect(
+ Ability.allowed?(user, :read_project, project)
+ ).to be_truthy
+ end
+
+ it 'uses AuthorizedProjectsWorker' do
+ expect(AuthorizedProjectsWorker).to(
+ receive(:bulk_perform_async).with(array_including([user.id], [other_user.id])).and_call_original
+ )
+ expect(AuthorizedProjectUpdate::ProjectCreateWorker).not_to(
+ receive(:perform_async)
+ )
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).not_to(
+ receive(:bulk_perform_in)
+ )
+
+ create_project(user, opts)
+ end
+ end
+ end
+
def create_project(user, opts)
Projects::CreateService.new(user, opts).execute
end
diff --git a/spec/services/user_project_access_changed_service_spec.rb b/spec/services/user_project_access_changed_service_spec.rb
index 902ed723e09..f27eeb74265 100644
--- a/spec/services/user_project_access_changed_service_spec.rb
+++ b/spec/services/user_project_access_changed_service_spec.rb
@@ -17,5 +17,14 @@ describe UserProjectAccessChangedService do
described_class.new([1, 2]).execute(blocking: false)
end
+
+ it 'permits low-priority operation' do
+ expect(AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker).to(
+ receive(:bulk_perform_in).with(described_class::DELAY, [[1], [2]])
+ )
+
+ described_class.new([1, 2]).execute(blocking: false,
+ priority: described_class::LOW_PRIORITY)
+ end
end
end
diff --git a/spec/support/helpers/usage_data_helpers.rb b/spec/support/helpers/usage_data_helpers.rb
index 067ae342f57..382e4f6a1a4 100644
--- a/spec/support/helpers/usage_data_helpers.rb
+++ b/spec/support/helpers/usage_data_helpers.rb
@@ -99,6 +99,24 @@ module UsageDataHelpers
projects_with_error_tracking_enabled
projects_with_alerts_service_enabled
projects_with_prometheus_alerts
+ projects_with_expiration_policy_enabled
+ projects_with_expiration_policy_disabled
+ projects_with_expiration_policy_enabled_with_keep_n_unset
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_1
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_5
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_10
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_25
+ projects_with_expiration_policy_enabled_with_keep_n_set_to_50
+ projects_with_expiration_policy_enabled_with_older_than_unset
+ projects_with_expiration_policy_enabled_with_older_than_set_to_7d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_14d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_30d
+ projects_with_expiration_policy_enabled_with_older_than_set_to_90d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_1d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_7d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_14d
+ projects_with_expiration_policy_enabled_with_cadence_set_to_1month
+ projects_with_expiration_policy_enabled_with_cadence_set_to_3month
pages_domains
protected_branches
releases
@@ -136,25 +154,6 @@ module UsageDataHelpers
prometheus_metrics_enabled
web_ide_clientside_preview_enabled
ingress_modsecurity_enabled
- projects_with_expiration_policy_disabled
- projects_with_expiration_policy_enabled
- projects_with_expiration_policy_enabled_with_keep_n_unset
- projects_with_expiration_policy_enabled_with_older_than_unset
- projects_with_expiration_policy_enabled_with_keep_n_set_to_1
- projects_with_expiration_policy_enabled_with_keep_n_set_to_5
- projects_with_expiration_policy_enabled_with_keep_n_set_to_10
- projects_with_expiration_policy_enabled_with_keep_n_set_to_25
- projects_with_expiration_policy_enabled_with_keep_n_set_to_50
- projects_with_expiration_policy_enabled_with_keep_n_set_to_100
- projects_with_expiration_policy_enabled_with_cadence_set_to_1d
- projects_with_expiration_policy_enabled_with_cadence_set_to_7d
- projects_with_expiration_policy_enabled_with_cadence_set_to_14d
- projects_with_expiration_policy_enabled_with_cadence_set_to_1month
- projects_with_expiration_policy_enabled_with_cadence_set_to_3month
- projects_with_expiration_policy_enabled_with_older_than_set_to_7d
- projects_with_expiration_policy_enabled_with_older_than_set_to_14d
- projects_with_expiration_policy_enabled_with_older_than_set_to_30d
- projects_with_expiration_policy_enabled_with_older_than_set_to_90d
object_store
).freeze
diff --git a/spec/support/shared_examples/features/error_tracking_shared_example.rb b/spec/support/shared_examples/features/error_tracking_shared_example.rb
index c33bd05f202..1cd05b22ae9 100644
--- a/spec/support/shared_examples/features/error_tracking_shared_example.rb
+++ b/spec/support/shared_examples/features/error_tracking_shared_example.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
shared_examples 'error tracking index page' do
- it 'renders the error index page' do
+ it 'renders the error index page', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.js-title-container') do
expect(page).to have_content(project.namespace.name)
expect(page).to have_content(project.name)
@@ -15,7 +15,7 @@ shared_examples 'error tracking index page' do
end
end
- it 'loads the error show page on click' do
+ it 'loads the error show page on click', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
click_on issues_response[0]['title']
wait_for_requests
@@ -23,7 +23,7 @@ shared_examples 'error tracking index page' do
expect(page).to have_content('Error Details')
end
- it 'renders the error index data' do
+ it 'renders the error index data', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.error-list') do
expect(page).to have_content(issues_response[0]['title'])
expect(page).to have_content(issues_response[0]['count'].to_s)
@@ -34,7 +34,7 @@ shared_examples 'error tracking index page' do
end
shared_examples 'expanded stack trace context' do |selected_line: nil, expected_line: 1|
- it 'expands the stack trace context' do
+ it 'expands the stack trace context', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
within('div.stacktrace') do
find("div.file-holder:nth-child(#{selected_line}) svg.ic-chevron-right").click if selected_line
@@ -49,7 +49,7 @@ shared_examples 'expanded stack trace context' do |selected_line: nil, expected_
end
shared_examples 'error tracking show page' do
- it 'renders the error details' do
+ it 'renders the error details', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
content = page.find(".content")
nav = page.find("nav.breadcrumbs")
header = page.find(".error-details-header")
@@ -67,11 +67,11 @@ shared_examples 'error tracking show page' do
expect(content).to have_content('Users: 0')
end
- it 'renders the stack trace heading' do
+ it 'renders the stack trace heading', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
expect(page).to have_content('Stack trace')
end
- it 'renders the stack trace', :quarantine do
+ it 'renders the stack trace', quarantine: { issue: 'https://gitlab.com/gitlab-org/gitlab/-/issues/217810' } do
event_response['entries'][0]['data']['values'][0]['stacktrace']['frames'].each do |frame|
expect(frame['filename']).not_to be_nil
expect(page).to have_content(frame['filename'])
diff --git a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
index ecf1640ef5d..21ab9b06c33 100644
--- a/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
+++ b/spec/support/shared_examples/models/issuable_hook_data_shared_examples.rb
@@ -7,6 +7,7 @@ RSpec.shared_examples 'issuable hook data' do |kind|
include_examples 'project hook data' do
let(:project) { builder.issuable.project }
end
+
include_examples 'deprecated repository hook data'
context "with a #{kind}" do
diff --git a/yarn.lock b/yarn.lock
index fdeaf56eb44..25789a2d4fc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -787,10 +787,10 @@
resolved "https://registry.yarnpkg.com/@gitlab/svgs/-/svgs-1.127.0.tgz#1f7ffdffe44d6a82b372535f93d78f3a895d1960"
integrity sha512-Uv52DqkG2KwCB0VRlXUEHFZxJ/7Ql0t1YTdzICpXmDjltuUBrysFcdmWPVO6PgXQxk2ahryNsUjSOziMYTeSiw==
-"@gitlab/ui@14.5.0":
- version "14.5.0"
- resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.5.0.tgz#6fed9a5b435884fe69e499578469c8b144726c90"
- integrity sha512-7OarJzyyeRpFRmShN7c2GBPpahBDbmOSk10ATisannbX/h9i+Z83MQ8ZDqYbM8qeRIfG/BVsnLjC8M7aSsBlPQ==
+"@gitlab/ui@14.10.0":
+ version "14.10.0"
+ resolved "https://registry.yarnpkg.com/@gitlab/ui/-/ui-14.10.0.tgz#39c04d62c914fcefe96c7ec32fdf31b1f98f1119"
+ integrity sha512-k9w6z3/QBeUas++cH5BaozjxY4fVu+AggjGoh9QMKN5hpiGTiTPx5aQJIlOv8UX/kpUmgc4pHSWAbw30YVGGFw==
dependencies:
"@babel/standalone" "^7.0.0"
"@gitlab/vue-toasted" "^1.3.0"