summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/ci/frontend.gitlab-ci.yml6
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_gfm.js4
-rw-r--r--app/assets/javascripts/behaviors/markdown/render_metrics.js24
-rw-r--r--app/assets/javascripts/monitoring/components/charts/area.vue96
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue7
-rw-r--r--app/assets/javascripts/monitoring/components/embed.vue97
-rw-r--r--app/assets/javascripts/monitoring/constants.js2
-rw-r--r--app/assets/javascripts/monitoring/stores/actions.js12
-rw-r--r--app/assets/javascripts/monitoring/stores/mutation_types.js1
-rw-r--r--app/assets/javascripts/monitoring/stores/mutations.js3
-rw-r--r--app/assets/javascripts/monitoring/stores/state.js1
-rw-r--r--app/assets/stylesheets/pages/prometheus.scss5
-rw-r--r--app/controllers/admin/groups_controller.rb3
-rw-r--r--app/controllers/groups_controller.rb3
-rw-r--r--app/controllers/projects/snippets_controller.rb3
-rw-r--r--app/controllers/projects/wikis_controller.rb3
-rw-r--r--app/controllers/projects_controller.rb8
-rw-r--r--app/controllers/registrations_controller.rb3
-rw-r--r--app/controllers/sessions_controller.rb7
-rw-r--r--app/controllers/snippets_controller.rb3
-rw-r--r--app/finders/autocomplete/move_to_project_finder.rb9
-rw-r--r--app/helpers/submodule_helper.rb12
-rw-r--r--app/models/group.rb4
-rw-r--r--app/models/pages_domain.rb2
-rw-r--r--app/models/project.rb17
-rw-r--r--app/policies/group_policy.rb5
-rw-r--r--app/views/admin/groups/show.html.haml5
-rw-r--r--app/views/admin/projects/show.html.haml5
-rw-r--r--app/views/groups/_group_admin_settings.html.haml6
-rw-r--r--app/views/groups/settings/_permissions.html.haml1
-rw-r--r--app/views/groups/settings/_subgroup_creation_level.html.haml3
-rw-r--r--app/views/shared/issuable/_form.html.haml2
-rw-r--r--app/views/shared/projects/_project.html.haml2
-rw-r--r--changelogs/unreleased/55564-remove-if-in-before-after-action.yml5
-rw-r--r--changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml5
-rw-r--r--changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml5
-rw-r--r--changelogs/unreleased/64870-can-t-save-pages-domain-form-with-let-s-encrypt-enabled-if-current-certificate-is-outdated.yml6
-rw-r--r--changelogs/unreleased/dm-submodule-helper-routing.yml5
-rw-r--r--changelogs/unreleased/fix-i18n-updated-projects.yml5
-rw-r--r--changelogs/unreleased/jprovazn-project-search.yml5
-rw-r--r--changelogs/unreleased/maintainers-can-create-subgroup.yml5
-rw-r--r--db/migrate/20190626175626_add_group_creation_level_to_namespaces.rb18
-rw-r--r--db/schema.rb1
-rw-r--r--doc/development/contributing/issue_workflow.md59
-rw-r--r--doc/user/group/subgroups/index.md24
-rw-r--r--doc/user/permissions.md5
-rw-r--r--doc/user/project/merge_requests/blocking_merge_requests.md133
-rw-r--r--doc/user/project/merge_requests/img/edit_blocking_merge_requests.pngbin0 -> 9926 bytes
-rw-r--r--doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.pngbin0 -> 10867 bytes
-rw-r--r--doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.pngbin0 -> 27089 bytes
-rw-r--r--doc/user/project/merge_requests/index.md16
-rw-r--r--doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md23
-rw-r--r--lib/gitlab/access.rb11
-rw-r--r--locale/gitlab.pot15
-rw-r--r--spec/controllers/admin/groups_controller_spec.rb8
-rw-r--r--spec/controllers/autocomplete_controller_spec.rb22
-rw-r--r--spec/factories/groups.rb4
-rw-r--r--spec/factories/pages_domains.rb84
-rw-r--r--spec/features/admin/admin_groups_spec.rb9
-rw-r--r--spec/features/admin/admin_projects_spec.rb1
-rw-r--r--spec/features/groups/group_settings_spec.rb8
-rw-r--r--spec/features/groups/show_spec.rb99
-rw-r--r--spec/features/projects/tree/create_directory_spec.rb4
-rw-r--r--spec/features/projects/tree/create_file_spec.rb4
-rw-r--r--spec/finders/autocomplete/move_to_project_finder_spec.rb40
-rw-r--r--spec/frontend/behaviors/markdown/render_metrics_spec.js37
-rw-r--r--spec/frontend/monitoring/embed/embed_spec.js78
-rw-r--r--spec/frontend/monitoring/embed/mock_data.js87
-rw-r--r--spec/frontend/test_setup.js6
-rw-r--r--spec/helpers/submodule_helper_spec.rb81
-rw-r--r--spec/javascripts/helpers/vue_test_utils_helper.js18
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-rw-r--r--spec/models/group_spec.rb7
-rw-r--r--spec/models/pages_domain_spec.rb24
-rw-r--r--spec/models/project_spec.rb20
-rw-r--r--spec/policies/group_policy_spec.rb127
-rw-r--r--spec/requests/api/groups_spec.rb4
-rw-r--r--spec/services/groups/create_service_spec.rb8
-rw-r--r--spec/support/capybara.rb1
-rw-r--r--spec/support/shared_contexts/policies/group_policy_shared_context.rb2
80 files changed, 1200 insertions, 294 deletions
diff --git a/.gitlab/ci/frontend.gitlab-ci.yml b/.gitlab/ci/frontend.gitlab-ci.yml
index b00d2f46088..5d4bbc06e93 100644
--- a/.gitlab/ci/frontend.gitlab-ci.yml
+++ b/.gitlab/ci/frontend.gitlab-ci.yml
@@ -210,15 +210,15 @@ qa:selectors:
qa-frontend-node:8:
<<: *qa-frontend-node
- image: node:8-alpine
+ image: node:carbon
qa-frontend-node:10:
<<: *qa-frontend-node
- image: node:10-alpine
+ image: node:dubnium
qa-frontend-node:latest:
<<: *qa-frontend-node
- image: node:alpine
+ image: node:latest
allow_failure: true
lint:javascript:report:
diff --git a/app/assets/javascripts/behaviors/markdown/render_gfm.js b/app/assets/javascripts/behaviors/markdown/render_gfm.js
index bfb073fdcdc..789a057caf8 100644
--- a/app/assets/javascripts/behaviors/markdown/render_gfm.js
+++ b/app/assets/javascripts/behaviors/markdown/render_gfm.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import syntaxHighlight from '~/syntax_highlight';
import renderMath from './render_math';
import renderMermaid from './render_mermaid';
+import renderMetrics from './render_metrics';
import highlightCurrentUser from './highlight_current_user';
import initUserPopovers from '../../user_popovers';
import initMRPopovers from '../../mr_popover';
@@ -17,6 +18,9 @@ $.fn.renderGFM = function renderGFM() {
highlightCurrentUser(this.find('.gfm-project_member').get());
initUserPopovers(this.find('.gfm-project_member').get());
initMRPopovers(this.find('.gfm-merge_request').get());
+ if (gon.features && gon.features.gfmEmbeddedMetrics) {
+ renderMetrics(this.find('.js-render-metrics').get());
+ }
return this;
};
diff --git a/app/assets/javascripts/behaviors/markdown/render_metrics.js b/app/assets/javascripts/behaviors/markdown/render_metrics.js
new file mode 100644
index 00000000000..252b98610b6
--- /dev/null
+++ b/app/assets/javascripts/behaviors/markdown/render_metrics.js
@@ -0,0 +1,24 @@
+import Vue from 'vue';
+import Metrics from '~/monitoring/components/embed.vue';
+import { createStore } from '~/monitoring/stores';
+
+// TODO: Handle copy-pasting - https://gitlab.com/gitlab-org/gitlab-ce/issues/64369.
+export default function renderMetrics(elements) {
+ if (!elements.length) {
+ return;
+ }
+
+ elements.forEach(element => {
+ const { dashboardUrl } = element.dataset;
+ const MetricsComponent = Vue.extend(Metrics);
+
+ // eslint-disable-next-line no-new
+ new MetricsComponent({
+ el: element,
+ store: createStore(),
+ propsData: {
+ dashboardUrl,
+ },
+ });
+ });
+}
diff --git a/app/assets/javascripts/monitoring/components/charts/area.vue b/app/assets/javascripts/monitoring/components/charts/area.vue
index 454ff4f284e..edf9423c74c 100644
--- a/app/assets/javascripts/monitoring/components/charts/area.vue
+++ b/app/assets/javascripts/monitoring/components/charts/area.vue
@@ -37,7 +37,13 @@ export default {
},
projectPath: {
type: String,
- required: true,
+ required: false,
+ default: () => '',
+ },
+ showBorder: {
+ type: Boolean,
+ required: false,
+ default: () => false,
},
thresholds: {
type: Array,
@@ -234,52 +240,54 @@ export default {
</script>
<template>
- <div class="prometheus-graph col-12 col-lg-6">
- <div class="prometheus-graph-header">
- <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
- <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
- </div>
- <gl-area-chart
- ref="areaChart"
- v-bind="$attrs"
- :data="chartData"
- :option="chartOptions"
- :format-tooltip-text="formatTooltipText"
- :thresholds="thresholds"
- :width="width"
- :height="height"
- @updated="onChartUpdated"
- >
- <template v-if="tooltip.isDeployment">
- <template slot="tooltipTitle">
- {{ __('Deployed') }}
- </template>
- <div slot="tooltipContent" class="d-flex align-items-center">
- <icon name="commit" class="mr-2" />
- <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
- </div>
- </template>
- <template v-else>
- <template slot="tooltipTitle">
- <div class="text-nowrap">
- {{ tooltip.title }}
+ <div class="col-12 col-lg-6" :class="[showBorder ? 'p-2' : 'p-0']">
+ <div class="prometheus-graph" :class="{ 'prometheus-graph-embed w-100 p-3': showBorder }">
+ <div class="prometheus-graph-header">
+ <h5 ref="graphTitle" class="prometheus-graph-title">{{ graphData.title }}</h5>
+ <div ref="graphWidgets" class="prometheus-graph-widgets"><slot></slot></div>
+ </div>
+ <gl-area-chart
+ ref="areaChart"
+ v-bind="$attrs"
+ :data="chartData"
+ :option="chartOptions"
+ :format-tooltip-text="formatTooltipText"
+ :thresholds="thresholds"
+ :width="width"
+ :height="height"
+ @updated="onChartUpdated"
+ >
+ <template v-if="tooltip.isDeployment">
+ <template slot="tooltipTitle">
+ {{ __('Deployed') }}
+ </template>
+ <div slot="tooltipContent" class="d-flex align-items-center">
+ <icon name="commit" class="mr-2" />
+ <gl-link :href="tooltip.commitUrl">{{ tooltip.sha }}</gl-link>
</div>
</template>
- <template slot="tooltipContent">
- <div
- v-for="(content, key) in tooltip.content"
- :key="key"
- class="d-flex justify-content-between"
- >
- <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
- {{ content.name }}
- </gl-chart-series-label>
- <div class="prepend-left-32">
- {{ content.value }}
+ <template v-else>
+ <template slot="tooltipTitle">
+ <div class="text-nowrap">
+ {{ tooltip.title }}
</div>
- </div>
+ </template>
+ <template slot="tooltipContent">
+ <div
+ v-for="(content, key) in tooltip.content"
+ :key="key"
+ class="d-flex justify-content-between"
+ >
+ <gl-chart-series-label :color="isMultiSeries ? content.color : ''">
+ {{ content.name }}
+ </gl-chart-series-label>
+ <div class="prepend-left-32">
+ {{ content.value }}
+ </div>
+ </div>
+ </template>
</template>
- </template>
- </gl-area-chart>
+ </gl-area-chart>
+ </div>
</div>
</template>
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 6eaced0c108..c7c55880040 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -11,10 +11,9 @@ import MonitorSingleStatChart from './charts/single_stat.vue';
import PanelType from './panel_type.vue';
import GraphGroup from './graph_group.vue';
import EmptyState from './empty_state.vue';
-import { timeWindows, timeWindowsKeyNames } from '../constants';
+import { sidebarAnimationDuration, timeWindows, timeWindowsKeyNames } from '../constants';
import { getTimeDiff } from '../utils';
-const sidebarAnimationDuration = 150;
let sidebarMutationObserver;
export default {
@@ -370,8 +369,8 @@ export default {
</div>
<div v-if="!showEmptyState">
<graph-group
- v-for="(groupData, index) in groupsWithData"
- :key="index"
+ v-for="groupData in groupsWithData"
+ :key="`${groupData.group}.${groupData.priority}`"
:name="groupData.group"
:show-panels="showPanels"
>
diff --git a/app/assets/javascripts/monitoring/components/embed.vue b/app/assets/javascripts/monitoring/components/embed.vue
new file mode 100644
index 00000000000..e17f03de0fd
--- /dev/null
+++ b/app/assets/javascripts/monitoring/components/embed.vue
@@ -0,0 +1,97 @@
+<script>
+import { mapActions, mapState } from 'vuex';
+import GraphGroup from './graph_group.vue';
+import MonitorAreaChart from './charts/area.vue';
+import { sidebarAnimationDuration, timeWindowsKeyNames, timeWindows } from '../constants';
+import { getTimeDiff } from '../utils';
+
+let sidebarMutationObserver;
+
+export default {
+ components: {
+ GraphGroup,
+ MonitorAreaChart,
+ },
+ props: {
+ dashboardUrl: {
+ type: String,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ params: {
+ ...getTimeDiff(timeWindows[timeWindowsKeyNames.eightHours]),
+ },
+ elWidth: 0,
+ };
+ },
+ computed: {
+ ...mapState('monitoringDashboard', ['groups', 'metricsWithData']),
+ groupData() {
+ const groupsWithData = this.groups.filter(group => this.chartsWithData(group.metrics).length);
+ if (groupsWithData.length) {
+ return groupsWithData[0];
+ }
+ return null;
+ },
+ },
+ mounted() {
+ this.setInitialState();
+ this.fetchMetricsData(this.params);
+ sidebarMutationObserver = new MutationObserver(this.onSidebarMutation);
+ sidebarMutationObserver.observe(document.querySelector('.layout-page'), {
+ attributes: true,
+ childList: false,
+ subtree: false,
+ });
+ },
+ beforeDestroy() {
+ if (sidebarMutationObserver) {
+ sidebarMutationObserver.disconnect();
+ }
+ },
+ methods: {
+ ...mapActions('monitoringDashboard', [
+ 'fetchMetricsData',
+ 'setEndpoints',
+ 'setFeatureFlags',
+ 'setShowErrorBanner',
+ ]),
+ chartsWithData(charts) {
+ return charts.filter(chart =>
+ chart.metrics.some(metric => this.metricsWithData.includes(metric.metric_id)),
+ );
+ },
+ onSidebarMutation() {
+ setTimeout(() => {
+ this.elWidth = this.$el.clientWidth;
+ }, sidebarAnimationDuration);
+ },
+ setInitialState() {
+ this.setFeatureFlags({
+ prometheusEndpointEnabled: true,
+ });
+ this.setEndpoints({
+ dashboardEndpoint: this.dashboardUrl,
+ });
+ this.setShowErrorBanner(false);
+ },
+ },
+};
+</script>
+<template>
+ <div class="metrics-embed">
+ <div v-if="groupData" class="row w-100 m-n2 pb-4">
+ <monitor-area-chart
+ v-for="graphData in chartsWithData(groupData.metrics)"
+ :key="graphData.title"
+ :graph-data="graphData"
+ :container-width="elWidth"
+ group-id="monitor-area-chart"
+ :project-path="null"
+ :show-border="true"
+ />
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/monitoring/constants.js b/app/assets/javascripts/monitoring/constants.js
index 26f1bf3f68d..605c95e6da5 100644
--- a/app/assets/javascripts/monitoring/constants.js
+++ b/app/assets/javascripts/monitoring/constants.js
@@ -1,5 +1,7 @@
import { __ } from '~/locale';
+export const sidebarAnimationDuration = 300; // milliseconds.
+
export const chartHeight = 300;
export const graphTypes = {
diff --git a/app/assets/javascripts/monitoring/stores/actions.js b/app/assets/javascripts/monitoring/stores/actions.js
index 5b3da51e9a6..245cc2eaca3 100644
--- a/app/assets/javascripts/monitoring/stores/actions.js
+++ b/app/assets/javascripts/monitoring/stores/actions.js
@@ -44,6 +44,10 @@ export const setFeatureFlags = (
commit(types.SET_ADDITIONAL_PANEL_TYPES_ENABLED, additionalPanelTypesEnabled);
};
+export const setShowErrorBanner = ({ commit }, enabled) => {
+ commit(types.SET_SHOW_ERROR_BANNER, enabled);
+};
+
export const requestMetricsDashboard = ({ commit }) => {
commit(types.REQUEST_METRICS_DATA);
};
@@ -99,7 +103,9 @@ export const fetchMetricsData = ({ state, dispatch }, params) => {
})
.catch(error => {
dispatch('receiveMetricsDataFailure', error);
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ if (state.setShowErrorBanner) {
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ }
});
};
@@ -119,7 +125,9 @@ export const fetchDashboard = ({ state, dispatch }, params) => {
})
.catch(error => {
dispatch('receiveMetricsDashboardFailure', error);
- createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ if (state.setShowErrorBanner) {
+ createFlash(s__('Metrics|There was an error while retrieving metrics'));
+ }
});
};
diff --git a/app/assets/javascripts/monitoring/stores/mutation_types.js b/app/assets/javascripts/monitoring/stores/mutation_types.js
index c89daba3df7..4b1aadbcf05 100644
--- a/app/assets/javascripts/monitoring/stores/mutation_types.js
+++ b/app/assets/javascripts/monitoring/stores/mutation_types.js
@@ -16,3 +16,4 @@ export const SET_ALL_DASHBOARDS = 'SET_ALL_DASHBOARDS';
export const SET_ENDPOINTS = 'SET_ENDPOINTS';
export const SET_GETTING_STARTED_EMPTY_STATE = 'SET_GETTING_STARTED_EMPTY_STATE';
export const SET_NO_DATA_EMPTY_STATE = 'SET_NO_DATA_EMPTY_STATE';
+export const SET_SHOW_ERROR_BANNER = 'SET_SHOW_ERROR_BANNER';
diff --git a/app/assets/javascripts/monitoring/stores/mutations.js b/app/assets/javascripts/monitoring/stores/mutations.js
index 0104dcb867d..b19520d6638 100644
--- a/app/assets/javascripts/monitoring/stores/mutations.js
+++ b/app/assets/javascripts/monitoring/stores/mutations.js
@@ -96,4 +96,7 @@ export default {
[types.SET_ADDITIONAL_PANEL_TYPES_ENABLED](state, enabled) {
state.additionalPanelTypesEnabled = enabled;
},
+ [types.SET_SHOW_ERROR_BANNER](state, enabled) {
+ state.showErrorBanner = enabled;
+ },
};
diff --git a/app/assets/javascripts/monitoring/stores/state.js b/app/assets/javascripts/monitoring/stores/state.js
index e54bb712695..440bdc951e0 100644
--- a/app/assets/javascripts/monitoring/stores/state.js
+++ b/app/assets/javascripts/monitoring/stores/state.js
@@ -12,6 +12,7 @@ export default () => ({
additionalPanelTypesEnabled: false,
emptyState: 'gettingStarted',
showEmptyState: true,
+ showErrorBanner: true,
groups: [],
deploymentData: [],
environments: [],
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index 2d600e3aef6..05a4cc168a8 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -29,6 +29,11 @@
padding: $gl-padding / 2;
}
+.prometheus-graph-embed {
+ border: 1px solid $border-color;
+ border-radius: $border-radius-default;
+}
+
.prometheus-graph-header {
display: flex;
align-items: center;
diff --git a/app/controllers/admin/groups_controller.rb b/app/controllers/admin/groups_controller.rb
index 15f7ef881c8..6317fa7c8d1 100644
--- a/app/controllers/admin/groups_controller.rb
+++ b/app/controllers/admin/groups_controller.rb
@@ -90,7 +90,8 @@ class Admin::GroupsController < Admin::ApplicationController
:visibility_level,
:require_two_factor_authentication,
:two_factor_grace_period,
- :project_creation_level
+ :project_creation_level,
+ :subgroup_creation_level
]
end
end
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index dbddee47997..0176962cf0a 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -192,7 +192,8 @@ class GroupsController < Groups::ApplicationController
:chat_team_name,
:require_two_factor_authentication,
:two_factor_grace_period,
- :project_creation_level
+ :project_creation_level,
+ :subgroup_creation_level
]
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 255f1f3569a..59f948959d6 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -7,7 +7,8 @@ class Projects::SnippetsController < Projects::ApplicationController
include SnippetsActions
include RendersBlob
- skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+ skip_before_action :verify_authenticity_token,
+ if: -> { action_name == 'show' && js_request? }
before_action :check_snippets_available!
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw, :toggle_award_emoji, :mark_as_spam]
diff --git a/app/controllers/projects/wikis_controller.rb b/app/controllers/projects/wikis_controller.rb
index fa5bdbc7d49..b0998d7f3be 100644
--- a/app/controllers/projects/wikis_controller.rb
+++ b/app/controllers/projects/wikis_controller.rb
@@ -10,7 +10,8 @@ class Projects::WikisController < Projects::ApplicationController
before_action :authorize_admin_wiki!, only: :destroy
before_action :load_project_wiki
before_action :load_page, only: [:show, :edit, :update, :history, :destroy]
- before_action :valid_encoding?, only: [:show, :edit, :update], if: :load_page
+ before_action :valid_encoding?,
+ if: -> { %w[show edit update].include?(action_name) && load_page }
before_action only: [:edit, :update], unless: :valid_encoding? do
redirect_to(project_wiki_path(@project, @page))
end
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index feefc7f8137..37ffd28bf9e 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -18,9 +18,11 @@ class ProjectsController < Projects::ApplicationController
before_action :redirect_git_extension, only: [:show]
before_action :project, except: [:index, :new, :create, :resolve]
before_action :repository, except: [:index, :new, :create, :resolve]
- before_action :assign_ref_vars, only: [:show], if: :repo_exists?
- before_action :tree, only: [:show], if: [:repo_exists?, :project_view_files?]
- before_action :lfs_blob_ids, only: [:show], if: [:repo_exists?, :project_view_files?]
+ before_action :assign_ref_vars, if: -> { action_name == 'show' && repo_exists? }
+ before_action :tree,
+ if: -> { action_name == 'show' && repo_exists? && project_view_files? }
+ before_action :lfs_blob_ids,
+ if: -> { action_name == 'show' && repo_exists? && project_view_files? }
before_action :project_export_enabled, only: [:export, :download_export, :remove_export, :generate_new_export]
before_action :present_project, only: [:edit]
before_action :authorize_download_code!, only: [:refs]
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index b2b151bbcf0..638934694e0 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -8,8 +8,7 @@ class RegistrationsController < Devise::RegistrationsController
prepend_before_action :check_captcha, only: :create
before_action :whitelist_query_limiting, only: [:destroy]
before_action :ensure_terms_accepted,
- if: -> { Gitlab::CurrentSettings.current_application_settings.enforce_terms? },
- only: [:create]
+ if: -> { action_name == 'create' && Gitlab::CurrentSettings.current_application_settings.enforce_terms? }
def new
redirect_to(new_user_session_path)
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index a841859621e..7604b31467a 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -13,18 +13,17 @@ class SessionsController < Devise::SessionsController
prepend_before_action :check_initial_setup, only: [:new]
prepend_before_action :authenticate_with_two_factor,
- if: :two_factor_enabled?, only: [:create]
+ if: -> { action_name == 'create' && two_factor_enabled? }
prepend_before_action :check_captcha, only: [:create]
prepend_before_action :store_redirect_uri, only: [:new]
prepend_before_action :ldap_servers, only: [:new, :create]
prepend_before_action :require_no_authentication_without_flash, only: [:new, :create]
- prepend_before_action :ensure_password_authentication_enabled!, if: :password_based_login?, only: [:create]
+ prepend_before_action :ensure_password_authentication_enabled!, if: -> { action_name == 'create' && password_based_login? }
before_action :auto_sign_in_with_provider, only: [:new]
before_action :load_recaptcha
- after_action :log_failed_login, only: [:new], if: :failed_login?
-
+ after_action :log_failed_login, if: -> { action_name == 'new' && failed_login? }
helper_method :captcha_enabled?
CAPTCHA_HEADER = 'X-GitLab-Show-Login-Captcha'.freeze
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index fad036b8df8..869655e9550 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -8,7 +8,8 @@ class SnippetsController < ApplicationController
include RendersBlob
include PreviewMarkdown
- skip_before_action :verify_authenticity_token, only: [:show], if: :js_request?
+ skip_before_action :verify_authenticity_token,
+ if: -> { action_name == 'show' && js_request? }
before_action :snippet, only: [:show, :edit, :destroy, :update, :raw]
diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb
index edaf74c5f92..491cce2232e 100644
--- a/app/finders/autocomplete/move_to_project_finder.rb
+++ b/app/finders/autocomplete/move_to_project_finder.rb
@@ -3,7 +3,9 @@
module Autocomplete
# Finder that retrieves a list of projects that an issue can be moved to.
class MoveToProjectFinder
- attr_reader :current_user, :search, :project_id, :offset_id
+ attr_reader :current_user, :search, :project_id
+
+ LIMIT = 20
# current_user - The User object of the user that wants to view the list of
# projects.
@@ -14,13 +16,10 @@ module Autocomplete
#
# * search: An optional search query to apply to the list of projects.
# * project_id: The ID of a project to exclude from the returned relation.
- # * offset_id: The ID of a project to use for pagination. When given, only
- # projects with a lower ID are included in the list.
def initialize(current_user, params = {})
@current_user = current_user
@search = params[:search]
@project_id = params[:project_id]
- @offset_id = params[:offset_id]
end
def execute
@@ -28,8 +27,8 @@ module Autocomplete
.projects_where_can_admin_issues
.optionally_search(search)
.excluding_project(project_id)
- .paginate_in_descending_order_using_id(before: offset_id)
.eager_load_namespace_and_owner
+ .sorted_by_name_asc_limited(LIMIT)
end
end
end
diff --git a/app/helpers/submodule_helper.rb b/app/helpers/submodule_helper.rb
index 35e04b0ced3..9a281065b90 100644
--- a/app/helpers/submodule_helper.rb
+++ b/app/helpers/submodule_helper.rb
@@ -34,8 +34,8 @@ module SubmoduleHelper
project.sub!(/\.git\z/, '')
if self_url?(url, namespace, project)
- [namespace_project_path(namespace, project),
- namespace_project_tree_path(namespace, project, submodule_item_id)]
+ [url_helpers.namespace_project_path(namespace, project),
+ url_helpers.namespace_project_tree_path(namespace, project, submodule_item_id)]
elsif relative_self_url?(url)
relative_self_links(url, submodule_item_id, repository.project)
elsif github_dot_com_url?(url)
@@ -99,8 +99,8 @@ module SubmoduleHelper
begin
[
- namespace_project_path(target_namespace_path, submodule_base),
- namespace_project_tree_path(target_namespace_path, submodule_base, commit)
+ url_helpers.namespace_project_path(target_namespace_path, submodule_base),
+ url_helpers.namespace_project_tree_path(target_namespace_path, submodule_base, commit)
]
rescue ActionController::UrlGenerationError
[nil, nil]
@@ -118,4 +118,8 @@ module SubmoduleHelper
rescue URI::InvalidURIError
nil
end
+
+ def url_helpers
+ Gitlab::Routing.url_helpers
+ end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index 9520db1bc0a..37f30552b39 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -416,6 +416,10 @@ class Group < Namespace
super || ::Gitlab::CurrentSettings.default_project_creation
end
+ def subgroup_creation_level
+ super || ::Gitlab::Access::OWNER_SUBGROUP_ACCESS
+ end
+
private
def update_two_factor_requirement
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index e6e491634ab..27c122d3559 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -22,7 +22,7 @@ class PagesDomain < ApplicationRecord
validate :validate_pages_domain
validate :validate_matching_key, if: ->(domain) { domain.certificate.present? || domain.key.present? }
- validate :validate_intermediates, if: ->(domain) { domain.certificate.present? }
+ validate :validate_intermediates, if: ->(domain) { domain.certificate.present? && domain.certificate_changed? }
attr_encrypted :key,
mode: :per_attribute_iv_and_salt,
diff --git a/app/models/project.rb b/app/models/project.rb
index 8dfe2212282..ece7507e55c 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -357,6 +357,7 @@ class Project < ApplicationRecord
scope :sorted_by_activity, -> { reorder(Arel.sql("GREATEST(COALESCE(last_activity_at, '1970-01-01'), COALESCE(last_repository_updated_at, '1970-01-01')) DESC")) }
scope :sorted_by_stars_desc, -> { reorder(star_count: :desc) }
scope :sorted_by_stars_asc, -> { reorder(star_count: :asc) }
+ scope :sorted_by_name_asc_limited, ->(limit) { reorder(name: :asc).limit(limit) }
scope :in_namespace, ->(namespace_ids) { where(namespace_id: namespace_ids) }
scope :personal, ->(user) { where(namespace_id: user.namespace_id) }
@@ -441,22 +442,6 @@ class Project < ApplicationRecord
without_deleted.find_by_id(id)
end
- # Paginates a collection using a `WHERE id < ?` condition.
- #
- # before - A project ID to use for filtering out projects with an equal or
- # greater ID. If no ID is given, all projects are included.
- #
- # limit - The maximum number of rows to include.
- def self.paginate_in_descending_order_using_id(
- before: nil,
- limit: Kaminari.config.default_per_page
- )
- relation = order_id_desc.limit(limit)
- relation = relation.where('projects.id < ?', before) if before
-
- relation
- end
-
def self.eager_load_namespace_and_owner
includes(namespace: :owner)
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 9219283992f..0add8bfad31 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -38,6 +38,10 @@ class GroupPolicy < BasePolicy
@subject.project_creation_level == ::Gitlab::Access::DEVELOPER_MAINTAINER_PROJECT_ACCESS
end
+ condition(:maintainer_can_create_group) do
+ @subject.subgroup_creation_level == ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS
+ end
+
rule { public_group }.policy do
enable :read_group
enable :read_list
@@ -105,6 +109,7 @@ class GroupPolicy < BasePolicy
end
rule { owner & nested_groups_supported }.enable :create_subgroup
+ rule { maintainer & maintainer_can_create_group & nested_groups_supported }.enable :create_subgroup
rule { public_group | logged_in_viewable }.enable :view_globally
diff --git a/app/views/admin/groups/show.html.haml b/app/views/admin/groups/show.html.haml
index 98230684d56..f9cc118a252 100644
--- a/app/views/admin/groups/show.html.haml
+++ b/app/views/admin/groups/show.html.haml
@@ -40,6 +40,11 @@
%strong
= @group.created_at.to_s(:medium)
+ %li
+ %span.light= _('ID:')
+ %strong
+ = @group.id
+
= render_if_exists 'admin/namespace_plan_info', namespace: @group
%li
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 0fae8060b32..3eff0a221d7 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -55,6 +55,11 @@
= @project.created_at.to_s(:medium)
%li
+ %span.light ID:
+ %strong
+ = @project.id
+
+ %li
%span.light http:
%strong
= link_to @project.http_url_to_repo, project_path(@project)
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index b8f632d11d3..733cb36cc3d 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -17,6 +17,12 @@
= f.select :project_creation_level, options_for_select(::Gitlab::Access.project_creation_options, @group.project_creation_level), {}, class: 'form-control'
.form-group.row
+ .col-sm-2.col-form-label
+ = f.label s_('SubgroupCreationlevel|Allowed to create subgroups')
+ .col-sm-10
+ = f.select :subgroup_creation_level, options_for_select(::Gitlab::Access.subgroup_creation_options, @group.subgroup_creation_level), {}, class: 'form-control'
+
+.form-group.row
.col-sm-2.col-form-label.pt-0
= f.label :require_two_factor_authentication, 'Two-factor authentication'
.col-sm-10
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 0da1f1ba7f5..d3375e00bad 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -20,6 +20,7 @@
= render_if_exists 'groups/settings/ip_restriction', f: f, group: @group
= render 'groups/settings/lfs', f: f
= render 'groups/settings/project_creation_level', f: f, group: @group
+ = render 'groups/settings/subgroup_creation_level', f: f, group: @group
= render 'groups/settings/two_factor_auth', f: f
= render_if_exists 'groups/member_lock_setting', f: f, group: @group
diff --git a/app/views/groups/settings/_subgroup_creation_level.html.haml b/app/views/groups/settings/_subgroup_creation_level.html.haml
new file mode 100644
index 00000000000..f36ad192bad
--- /dev/null
+++ b/app/views/groups/settings/_subgroup_creation_level.html.haml
@@ -0,0 +1,3 @@
+.form-group
+ = f.label s_('SubgroupCreationLevel|Allowed to create subgroups'), class: 'label-bold'
+ = f.select :subgroup_creation_level, options_for_select(::Gitlab::Access.subgroup_creation_options, group.subgroup_creation_level), {}, class: 'form-control'
diff --git a/app/views/shared/issuable/_form.html.haml b/app/views/shared/issuable/_form.html.haml
index 214e87052da..07a7b5ce9de 100644
--- a/app/views/shared/issuable/_form.html.haml
+++ b/app/views/shared/issuable/_form.html.haml
@@ -33,6 +33,8 @@
= render_if_exists 'shared/issuable/approvals', issuable: issuable, presenter: presenter, form: form
+= render_if_exists "shared/issuable/form/merge_request_blocks", issuable: issuable, form: form
+
= render 'shared/issuable/form/branch_chooser', issuable: issuable, form: form
= render 'shared/issuable/form/merge_params', issuable: issuable
diff --git a/app/views/shared/projects/_project.html.haml b/app/views/shared/projects/_project.html.haml
index 04f72420fe1..c4d1bdad2c4 100644
--- a/app/views/shared/projects/_project.html.haml
+++ b/app/views/shared/projects/_project.html.haml
@@ -89,4 +89,4 @@
%span.icon-wrapper.pipeline-status
= render 'ci/status/icon', status: project.commit.last_pipeline.detailed_status(current_user), type: 'commit', tooltip_placement: 'top', path: pipeline_path
.updated-note
- %span _('Updated') #{updated_tooltip}
+ %span #{_('Updated')} #{updated_tooltip}
diff --git a/changelogs/unreleased/55564-remove-if-in-before-after-action.yml b/changelogs/unreleased/55564-remove-if-in-before-after-action.yml
new file mode 100644
index 00000000000..a787faa8a9c
--- /dev/null
+++ b/changelogs/unreleased/55564-remove-if-in-before-after-action.yml
@@ -0,0 +1,5 @@
+---
+title: Rewrite `if:` argument in before_action and alike when `only:` is also used
+merge_request: 24412
+author: George Thomas @thegeorgeous
+type: other
diff --git a/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml b/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml
new file mode 100644
index 00000000000..17763b4c69e
--- /dev/null
+++ b/changelogs/unreleased/60948-display-groupid-on-group-admin-page.yml
@@ -0,0 +1,5 @@
+---
+title: Display group id on group admin page
+merge_request: 29735
+author: Zsolt Kovari
+type: added
diff --git a/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml b/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml
new file mode 100644
index 00000000000..3ff83ede2fa
--- /dev/null
+++ b/changelogs/unreleased/60949-display-projectid-on-project-admin-page.yml
@@ -0,0 +1,5 @@
+---
+title: Display project id on project admin page
+merge_request: 29734
+author: Zsolt Kovari
+type: added
diff --git a/changelogs/unreleased/64870-can-t-save-pages-domain-form-with-let-s-encrypt-enabled-if-current-certificate-is-outdated.yml b/changelogs/unreleased/64870-can-t-save-pages-domain-form-with-let-s-encrypt-enabled-if-current-certificate-is-outdated.yml
new file mode 100644
index 00000000000..291901d64ed
--- /dev/null
+++ b/changelogs/unreleased/64870-can-t-save-pages-domain-form-with-let-s-encrypt-enabled-if-current-certificate-is-outdated.yml
@@ -0,0 +1,6 @@
+---
+title: Fix "Certificate misses intermediates" UI error when enabling Let's Encrypt
+ integration for pages domain
+merge_request: 30995
+author:
+type: fixed
diff --git a/changelogs/unreleased/dm-submodule-helper-routing.yml b/changelogs/unreleased/dm-submodule-helper-routing.yml
new file mode 100644
index 00000000000..779d4d167df
--- /dev/null
+++ b/changelogs/unreleased/dm-submodule-helper-routing.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bug that caused diffs not to show on MRs with changes to submodules
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-i18n-updated-projects.yml b/changelogs/unreleased/fix-i18n-updated-projects.yml
new file mode 100644
index 00000000000..408ee438480
--- /dev/null
+++ b/changelogs/unreleased/fix-i18n-updated-projects.yml
@@ -0,0 +1,5 @@
+---
+title: Properly translate term in projects list
+merge_request: 30958
+author:
+type: fixed
diff --git a/changelogs/unreleased/jprovazn-project-search.yml b/changelogs/unreleased/jprovazn-project-search.yml
new file mode 100644
index 00000000000..f76d4858924
--- /dev/null
+++ b/changelogs/unreleased/jprovazn-project-search.yml
@@ -0,0 +1,5 @@
+---
+title: Order projects in 'Move issue' dropdown by name.
+merge_request: 30778
+author:
+type: fixed
diff --git a/changelogs/unreleased/maintainers-can-create-subgroup.yml b/changelogs/unreleased/maintainers-can-create-subgroup.yml
new file mode 100644
index 00000000000..180f0f7247f
--- /dev/null
+++ b/changelogs/unreleased/maintainers-can-create-subgroup.yml
@@ -0,0 +1,5 @@
+---
+title: Maintainers can create subgroups
+merge_request: 29718
+author: Fabio Papa
+type: changed
diff --git a/db/migrate/20190626175626_add_group_creation_level_to_namespaces.rb b/db/migrate/20190626175626_add_group_creation_level_to_namespaces.rb
new file mode 100644
index 00000000000..3b75c92e518
--- /dev/null
+++ b/db/migrate/20190626175626_add_group_creation_level_to_namespaces.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+class AddGroupCreationLevelToNamespaces < ActiveRecord::Migration[5.1]
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ def up
+ add_column(:namespaces, :subgroup_creation_level, :integer)
+ change_column_default(:namespaces,
+ :subgroup_creation_level,
+ ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ end
+
+ def down
+ remove_column(:namespaces, :subgroup_creation_level)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index a5079d3a5bc..79cd1a3a797 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2119,6 +2119,7 @@ ActiveRecord::Schema.define(version: 2019_07_15_114644) do
t.string "ldap_sync_status", default: "ready", null: false
t.boolean "membership_lock", default: false
t.integer "last_ci_minutes_usage_notification_level"
+ t.integer "subgroup_creation_level", default: 1
t.index ["created_at"], name: "index_namespaces_on_created_at", using: :btree
t.index ["custom_project_templates_group_id", "type"], name: "index_namespaces_on_custom_project_templates_group_id_and_type", where: "(custom_project_templates_group_id IS NOT NULL)", using: :btree
t.index ["file_template_project_id"], name: "index_namespaces_on_file_template_project_id", using: :btree
diff --git a/doc/development/contributing/issue_workflow.md b/doc/development/contributing/issue_workflow.md
index abe11b8d1a8..79c701d7abf 100644
--- a/doc/development/contributing/issue_workflow.md
+++ b/doc/development/contributing/issue_workflow.md
@@ -102,20 +102,7 @@ any issue.
Stage labels specify which [DevOps stage][devops-stages] the issue belongs to.
-The current stage labels are:
-
-- ~"devops::manage"
-- ~"devops::plan"
-- ~"devops::create"
-- ~"devops::verify"
-- ~"devops::package"
-- ~"devops::release"
-- ~"devops::configure"
-- ~"devops::monitor"
-- ~"devops::secure"
-- ~"devops::defend"
-- ~"devops::growth"
-- ~"devops::enablement"
+The current stage labels can be found by [searching the labels list for `devops::`](https://gitlab.com/groups/gitlab-org/-/labels?search=devops%3A%3A).
These labels are [scoped labels](../../user/project/labels.md#scoped-labels-premium)
and thus are mutually exclusive.
@@ -138,44 +125,8 @@ The Stage labels are used to generate the [direction pages][direction-pages] aut
Group labels specify which [groups][structure-groups] the issue belongs to.
-The current group labels are:
-
-- ~"group::access"
-- ~"group::measure"
-- ~"group::source code"
-- ~"group::knowledge"
-- ~"group::editor"
-- ~"group::gitaly"
-- ~"group::gitter"
-- ~"group::team planning"
-- ~"group::enterprise planning"
-- ~"group::certify"
-- ~"group::ci and runner"
-- ~"group::testing"
-- ~"group::package"
-- ~"group::progressive delivery"
-- ~"group::release management"
-- ~"group::autodevops and kubernetes"
-- ~"group::serverless and paas"
-- ~"group::apm"
-- ~"group::health"
-- ~"group::static analysis"
-- ~"group::dynamic analysis"
-- ~"group::software composition analysis"
-- ~"group::runtime application security"
-- ~"group::threat management"
-- ~"group::application infrastructure security"
-- ~"group::activation"
-- ~"group::adoption"
-- ~"group::upsell"
-- ~"group::retention"
-- ~"group::fulfillment"
-- ~"group::telemetry"
-- ~"group::distribution"
-- ~"group::geo"
-- ~"group::memory"
-- ~"group::ecosystem"
-
+The current group labels can be found by [searching the labels list for `group::`](https://gitlab.com/groups/gitlab-org/-/labels?search=group%3A%3A).
+
These labels are [scoped labels](../../user/project/labels.md#scoped-labels-premium)
and thus are mutually exclusive.
@@ -248,7 +199,7 @@ If a bug seems to fall between two severity labels, assign it to the higher-seve
- Example(s) of ~S1
- Data corruption/loss.
- Security breach.
- - Unable to create an issue or merge request.
+ - Unable to create an issue or merge request.
- Unable to add a comment or thread to the issue or merge request.
- Example(s) of ~S2
- Cannot submit changes through the web IDE but the commandline works.
@@ -259,7 +210,7 @@ If a bug seems to fall between two severity labels, assign it to the higher-seve
- Example(s) of ~S4
- Label colors are incorrect.
- UI elements are not fully aligned.
-
+
## Label for community contributors
Issues that are beneficial to our users, 'nice to haves', that we currently do
diff --git a/doc/user/group/subgroups/index.md b/doc/user/group/subgroups/index.md
index 7c80c4b7928..2eb3fae1a85 100644
--- a/doc/user/group/subgroups/index.md
+++ b/doc/user/group/subgroups/index.md
@@ -74,14 +74,24 @@ structure.
## Creating a subgroup
-NOTE: **Note:**
-You must be an Owner of a group to create a subgroup. For
-more information check the [permissions table](../../permissions.md#group-members-permissions).
-For a list of words that are not allowed to be used as group names see the
+To create a subgroup you must either be an Owner or a Maintainer of the
+group, depending on the group's setting.
+
+By default, groups created in:
+
+- GitLab 12.2 or later allow both Owners and Maintainers to create subgroups.
+- GitLab 12.1 or earlier only allow Owners to create subgroups.
+
+This setting can be for any group by an Owner or Administrator.
+
+For more information check the
+[permissions table](../../permissions.md#group-members-permissions). For a list
+of words that are not allowed to be used as group names see the
[reserved names](../../reserved_names.md).
-Users can always create subgroups if they are explicitly added as an Owner to
-a parent group, even if group creation is disabled by an administrator in their
-settings.
+
+Users can always create subgroups if they are explicitly added as an Owner (or
+Maintainer, if that setting is enabled) to a parent group, even if group
+creation is disabled by an administrator in their settings.
To create a subgroup:
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 619cf34b5c3..044be05b932 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -209,13 +209,16 @@ group.
| Create project in group | | | ✓ | ✓ | ✓ |
| Create/edit/delete group milestones | | | ✓ | ✓ | ✓ |
| Enable/disable a dependency proxy **(PREMIUM)** | | | ✓ | ✓ | ✓ |
+| Create subgroup | | | | ✓ (1) | ✓ |
| Edit group | | | | | ✓ |
-| Create subgroup | | | | | ✓ |
| Manage group members | | | | | ✓ |
| Remove group | | | | | ✓ |
| Delete group epic **(ULTIMATE)** | | | | | ✓ |
| View group Audit Events | | | | | ✓ |
+- (1): Groups can be set to [allow either Owners or Owners and
+ Maintainers to create subgroups](group/subgroups/index.md#creating-a-subgroup)
+
### Subgroup permissions
When you add a member to a subgroup, they inherit the membership and
diff --git a/doc/user/project/merge_requests/blocking_merge_requests.md b/doc/user/project/merge_requests/blocking_merge_requests.md
new file mode 100644
index 00000000000..0506a7cb4a5
--- /dev/null
+++ b/doc/user/project/merge_requests/blocking_merge_requests.md
@@ -0,0 +1,133 @@
+---
+type: reference, concepts
+---
+
+# Blocking merge requests **(PREMIUM)**
+
+> Introduced in GitLab Premium 12.2
+
+Blocking merge requests allow dependencies between MRs to be expressed. If a
+merge request is blocked by another MR, it cannot be merged until that blocking
+MR is itself merged.
+
+NOTE: **Note:**
+Blocking merge requests are a **PREMIUM** feature, but this restriction is only
+enforced for the blocked merge request. A merge request in a **CORE** or
+**STARTER** project can block a **PREMIUM** merge request, but not vice-versa.
+
+## Use cases
+
+* Ensure changes to a library are merged before changes to a project that
+ imports the library
+* Prevent a documentation-only merge request from being merged before the MR
+ implementing the feature to be documented
+* Require an MR updating a permissions matrix to be merged before merging an
+ MR from someone who hasn't yet been granted permissions
+
+It is common for a single logical change to span several merge requests. These
+MRs may all be in a single project, or they may be spread out across multiple
+projects, and the order in which they are merged can be significant.
+
+For example, given a project `mycorp/awesome-project` that imports a library
+at `myfriend/awesome-lib`, adding a feature in `awesome-project` may **also**
+require changes to `awesome-lib`, and so necessitate two merge requests. Merging
+the `awesome-project` MR before the `awesome-lib` one would break the `master`
+branch.
+
+The `awesome-project` MR could be [marked as WIP](work_in_progress_merge_requests.md),
+and the reason for the WIP stated included in the comments. However, this
+requires the state of the `awesome-lib` MR to be manually tracked, and doesn't
+scale well if the `awesome-project` MR depends on changes to **several** other
+projects.
+
+By marking the `awesome-project` MR as blocked on the `awesome-lib` MR instead,
+the status of the dependency is automatically tracked by GitLab, and the WIP
+state can be used to communicate the readiness of the code in each individual
+MR instead.
+
+## Configuration
+
+To continue the above example, you can configure a block when creating the
+new MR in `awesome-project` (or by editing it, if it already exists). The block
+needs to be configured on the MR that will be **blocked**, rather than on the
+**blocking** MR. There is a "Blocking merge requests" section in the form:
+
+![Blocking merge requests form control](img/edit_blocking_merge_requests.png)
+
+Anyone who can edit a merge request can change the list of blocking merge
+requests.
+
+New blocks can be added by reference, by URL, or by using autcompletion. To
+remove a block, press the "X" by its reference.
+
+As blocks can be specified across projects, it's possible that someone else has
+added a block for a merge request in a project you don't have access to. These
+are shown as a simple count:
+
+![Blocking merge requests form control with inaccessible MRs](img/edit_blocking_merge_requests_inaccessible.png)
+
+If necessary, you can remove all the blocks like this by pressing the "X", just
+as you would for a single, visible block.
+
+Once you're finished, press the "Save changes" button to submit the request, or
+"Cancel" to return without making any changes.
+
+The list of configured blocks, and the status of each one, is shown in the merge
+request widget:
+
+![Blocking merge requests in merge request widget](img/show_blocking_merge_requests_in_mr_widget.png)
+
+Until all blocking merge requests have, themselves, been merged, the "Merge"
+button will be disabled. In particular, note that **closed** merge requests
+still block their dependents - it is impossible to automatically determine if
+merge requests that were blocked by that MR when it was open, are still blocked
+when it is closed.
+
+If a merge request has been closed **and** the block is no longer relevant, it
+must be removed as a blocking MR, following the instructions above, before
+merge.
+
+## Limitations
+
+* API support: [gitlab-ee#12551](https://gitlab.com/gitlab-org/gitlab-ee/issues/12551)
+* Blocking relationships are not preserved across project export/import: [gitlab-ee#12549](https://gitlab.com/gitlab-org/gitlab-ee/issues/12549)
+* Complex merge order dependencies are not supported: [gitlab-ee#11393](https://gitlab.com/gitlab-org/gitlab-ee/issues/11393)
+
+The last item merits a little more explanation. Blocking merge requests can be
+described as a graph of dependencies. The simplest possible graph has one
+merge request blocking another:
+
+```mermaid
+graph LR;
+ myfriend/awesome-lib!10-->mycorp/awesome-project!100;
+```
+
+A more complex (and still supported) graph might have several MRs blocking
+another from being merged:
+
+```mermaid
+graph LR;
+ myfriend/awesome-lib!10-->mycorp/awesome-project!100;
+ herfriend/another-lib!1-->mycorp/awesome-project!100;
+```
+
+We also support one MR blocking several others from being merged:
+
+```mermaid
+graph LR;
+ herfriend/another-lib!1-->myfriend/awesome-lib!10;
+ herfriend/another-lib!1-->mycorp/awesome-project!100;
+```
+
+What is **not** supported is a "deep", or "nested" graph of dependencies, e.g.:
+
+```mermaid
+graph LR;
+ herfriend/another-lib!1-->myfriend/awesome-lib!10;
+ myfriend/awesome-lib!10-->mycorp/awesome-project!100;
+```
+
+In this example, `myfriend/awesome-lib!10` would be blocked from being merged by
+`herfriend/another-lib!1`, and would also block `mycorp/awesome-project!100`
+from being merged. This is **not** yet supported.
+
diff --git a/doc/user/project/merge_requests/img/edit_blocking_merge_requests.png b/doc/user/project/merge_requests/img/edit_blocking_merge_requests.png
new file mode 100644
index 00000000000..0fe26d602e3
--- /dev/null
+++ b/doc/user/project/merge_requests/img/edit_blocking_merge_requests.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png b/doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png
new file mode 100644
index 00000000000..a1003c41c22
--- /dev/null
+++ b/doc/user/project/merge_requests/img/edit_blocking_merge_requests_inaccessible.png
Binary files differ
diff --git a/doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png b/doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png
new file mode 100644
index 00000000000..a1241f9e38c
--- /dev/null
+++ b/doc/user/project/merge_requests/img/show_blocking_merge_requests_in_mr_widget.png
Binary files differ
diff --git a/doc/user/project/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 8c28e24683f..08a5d2e03a3 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -47,6 +47,7 @@ With **[GitLab Enterprise Edition][ee]**, you can also:
- Analyze your dependencies for vulnerabilities with [Dependency Scanning](../../application_security/dependency_scanning/index.md) **(ULTIMATE)**
- Analyze your Docker images for vulnerabilities with [Container Scanning](../../application_security/container_scanning/index.md) **(ULTIMATE)**
- Determine the performance impact of changes with [Browser Performance Testing](#browser-performance-testing-premium) **(PREMIUM)**
+- Specify merge order dependencies with [Blocking Merge Requests](#blocking-merge-requests-premium) **(PREMIUM)**
## Use cases
@@ -412,6 +413,21 @@ GitLab runs the [Sitespeed.io container][sitespeed-container] and displays the d
[Read more about Browser Performance Testing.](browser_performance_testing.md)
+## Blocking Merge Requests **(PREMIUM)**
+
+> Introduced in [GitLab Premium][products] 12.2.
+
+A single logical change may be split across several merge requests, and perhaps
+even across several projects. When this happens, the order in which MRs are
+merged is important.
+
+GitLab allows you to specify that a merge request is blocked by other MRs. With
+this relationship in place, the merge request cannot be merged until all of its
+blockers have also been merged, helping to maintain the consistency of a single
+logical change.
+
+[Read more about Blocking Merge Requests.](blocking_merge_requests.md)
+
## Security reports **(ULTIMATE)**
GitLab can scan and report any vulnerabilities found in your project.
diff --git a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
index 7675a5dd9d4..4588375738e 100644
--- a/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
+++ b/doc/user/project/pages/custom_domains_ssl_tls_certification/lets_encrypt_integration.md
@@ -15,6 +15,12 @@ GitLab does it for you, out-of-the-box.
[Let's Encrypt](https://letsencrypt.org) is a free, automated, and
open source Certificate Authority.
+CAUTION: **Caution:**
+This feature is in **beta** and might present bugs and UX issues
+such as [#64870](https://gitlab.com/gitlab-org/gitlab-ce/issues/64870).
+See all the related issues linked from this [issue's description](https://gitlab.com/gitlab-org/gitlab-ce/issues/28996)
+for more information.
+
## Requirements
Before you can enable automatic provisioning of a SSL certificate for your domain, make sure you have:
@@ -55,9 +61,22 @@ associated Pages domain. It will be also renewed automatically by GitLab.
> - If you already have SSL certificate in domain settings it
> will continue to work until it will be replaced by Let's Encrypt's certificate.
-<!-- ## Troubleshooting
+## Troubleshooting
+
+### Error "Certificate misses intermediates"
+
+If you get an error **Certificate misses intermediates** while trying to enable Let's Encrypt integration for your domain, follow the steps below:
+
+1. Go to your project's **Settings > Pages**.
+1. Turn off **Force HTTPS** if it's turned on.
+1. Click **Details** on your domain.
+1. Click the **Edit** button in the top right corner of domain details page.
+1. Enable Let's Encrypt integration.
+1. Click **Save**.
+1. Go to your project's **Settings > Pages**.
+1. Turn on **Force HTTPS**.
-Include any troubleshooting steps that you can foresee. If you know beforehand what issues
+<!-- Include any troubleshooting steps that you can foresee. If you know beforehand what issues
one might have when setting this up, or when something is changed, or on upgrading, it's
important to describe those, too. Think of things that may go wrong and include them here.
This is important to minimize requests for support, and to avoid doc comments with
diff --git a/lib/gitlab/access.rb b/lib/gitlab/access.rb
index 6eb08f674c2..7ef9f7ef630 100644
--- a/lib/gitlab/access.rb
+++ b/lib/gitlab/access.rb
@@ -29,6 +29,10 @@ module Gitlab
MAINTAINER_PROJECT_ACCESS = 1
DEVELOPER_MAINTAINER_PROJECT_ACCESS = 2
+ # Default subgroup creation level
+ OWNER_SUBGROUP_ACCESS = 0
+ MAINTAINER_SUBGROUP_ACCESS = 1
+
class << self
delegate :values, to: :options
@@ -106,6 +110,13 @@ module Gitlab
def project_creation_level_name(name)
project_creation_options.key(name)
end
+
+ def subgroup_creation_options
+ {
+ s_('SubgroupCreationlevel|Owners') => OWNER_SUBGROUP_ACCESS,
+ s_('SubgroupCreationlevel|Maintainers') => MAINTAINER_SUBGROUP_ACCESS
+ }
+ end
end
def human_access
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 7b742660a4c..bd26ca6714d 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -5324,6 +5324,9 @@ msgstr ""
msgid "ID"
msgstr ""
+msgid "ID:"
+msgstr ""
+
msgid "IDE|Allow live previews of JavaScript projects in the Web IDE using CodeSandbox client side evaluation."
msgstr ""
@@ -10135,6 +10138,18 @@ msgstr ""
msgid "StorageSize|Unknown"
msgstr ""
+msgid "SubgroupCreationLevel|Allowed to create subgroups"
+msgstr ""
+
+msgid "SubgroupCreationlevel|Allowed to create subgroups"
+msgstr ""
+
+msgid "SubgroupCreationlevel|Maintainers"
+msgstr ""
+
+msgid "SubgroupCreationlevel|Owners"
+msgstr ""
+
msgid "Subgroups"
msgstr ""
diff --git a/spec/controllers/admin/groups_controller_spec.rb b/spec/controllers/admin/groups_controller_spec.rb
index 509d8944e3a..1123563c1e3 100644
--- a/spec/controllers/admin/groups_controller_spec.rb
+++ b/spec/controllers/admin/groups_controller_spec.rb
@@ -68,5 +68,13 @@ describe Admin::GroupsController do
post :update, params: { id: group.to_param, group: { project_creation_level: ::Gitlab::Access::NO_ONE_PROJECT_ACCESS } }
end.to change { group.reload.project_creation_level }.to(::Gitlab::Access::NO_ONE_PROJECT_ACCESS)
end
+
+ it 'updates the subgroup_creation_level successfully' do
+ expect do
+ post :update,
+ params: { id: group.to_param,
+ group: { subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS } }
+ end.to change { group.reload.subgroup_creation_level }.to(::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
+ end
end
end
diff --git a/spec/controllers/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 3f1c0ae8ac4..eaa5d6cd073 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -295,28 +295,6 @@ describe AutocompleteController do
end
end
- context 'authorized projects with offset' do
- before do
- authorized_project2 = create(:project)
- authorized_project3 = create(:project)
-
- authorized_project.add_maintainer(user)
- authorized_project2.add_maintainer(user)
- authorized_project3.add_maintainer(user)
- end
-
- describe 'GET #projects with project ID and offset_id' do
- before do
- get(:projects, params: { project_id: project.id, offset_id: authorized_project.id })
- end
-
- it 'returns projects' do
- expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq 2 # Of a total of 3
- end
- end
- end
-
context 'authorized projects without admin_issue ability' do
before do
authorized_project.add_guest(user)
diff --git a/spec/factories/groups.rb b/spec/factories/groups.rb
index 18a0c2ec731..b67ab955ffc 100644
--- a/spec/factories/groups.rb
+++ b/spec/factories/groups.rb
@@ -45,5 +45,9 @@ FactoryBot.define do
trait :auto_devops_disabled do
auto_devops_enabled false
end
+
+ trait :owner_subgroup_creation_only do
+ subgroup_creation_level ::Gitlab::Access::OWNER_SUBGROUP_ACCESS
+ end
end
end
diff --git a/spec/factories/pages_domains.rb b/spec/factories/pages_domains.rb
index 3e0baab04ce..e441dfcf229 100644
--- a/spec/factories/pages_domains.rb
+++ b/spec/factories/pages_domains.rb
@@ -166,6 +166,90 @@ pu/xO28QOG8=
-----END CERTIFICATE-----'
end
+ trait :with_trusted_expired_chain do
+ # This contains
+ # Let's Encrypt Authority X3
+ # DST Root CA X3
+ certificate '-----BEGIN CERTIFICATE-----
+MIIFSjCCBDKgAwIBAgISAw24xGWrFotvTBa6AZI/pzq1MA0GCSqGSIb3DQEBCwUA
+MEoxCzAJBgNVBAYTAlVTMRYwFAYDVQQKEw1MZXQncyBFbmNyeXB0MSMwIQYDVQQD
+ExpMZXQncyBFbmNyeXB0IEF1dGhvcml0eSBYMzAeFw0xOTAzMDcxNzU5NTZaFw0x
+OTA2MDUxNzU5NTZaMBQxEjAQBgNVBAMTCXN5dHNlLmNvbTCCASIwDQYJKoZIhvcN
+AQEBBQADggEPADCCAQoCggEBALtIpQuqeZN6OgEE+y2UoGC/31Vt9NAeQWvTuWWO
+nMn/MvDJiw8731Dx4DDbMjhF50UBE20a9iAu2nhlxcsuuIITk2MXKMEgPtqSbwM7
+Mg0/WvgrBOWnF9CpdD3qcsjtstT6Djij06VfMfUrRZzMkGgbGzudR0cShKPmkBVU
+LgB6crFmSQ/qHt5PzBivdexCUpz5WzSKU5UWYFx2UnkSLykvEJuUr3Nn4/o9oyKw
+Qoiq354S262mFuMW+s6wQdMNNkwj41OqCwAGbqq7YUYLDc8OQiRC2LcqSO5yYnnA
+0lNfbEatZ1BzHiDjTH7wMUtwcLGHsZ1C5ZmORD2s2gtGiRkCAwEAAaOCAl4wggJa
+MA4GA1UdDwEB/wQEAwIFoDAdBgNVHSUEFjAUBggrBgEFBQcDAQYIKwYBBQUHAwIw
+DAYDVR0TAQH/BAIwADAdBgNVHQ4EFgQUAMn3t1s4zXdOQbJFOP1riSwjuGkwHwYD
+VR0jBBgwFoAUqEpqYwR93brm0Tm3pkVl7/Oo7KEwbwYIKwYBBQUHAQEEYzBhMC4G
+CCsGAQUFBzABhiJodHRwOi8vb2NzcC5pbnQteDMubGV0c2VuY3J5cHQub3JnMC8G
+CCsGAQUFBzAChiNodHRwOi8vY2VydC5pbnQteDMubGV0c2VuY3J5cHQub3JnLzAU
+BgNVHREEDTALgglzeXRzZS5jb20wTAYDVR0gBEUwQzAIBgZngQwBAgEwNwYLKwYB
+BAGC3xMBAQEwKDAmBggrBgEFBQcCARYaaHR0cDovL2Nwcy5sZXRzZW5jcnlwdC5v
+cmcwggEEBgorBgEEAdZ5AgQCBIH1BIHyAPAAdQB0ftqDMa0zEJEhnM4lT0Jwwr/9
+XkIgCMY3NXnmEHvMVgAAAWlZhr4pAAAEAwBGMEQCIBEA+3oiM1UJKY1kajBO5Aoz
+9AZMMlImaR1X5hFIPr95AiBXGIACuXUDLchB0kT8VIG/jM4f9iuXMoYCoKNJggNM
+/gB3ACk8UZZUyDlluqpQ/FgH1Ldvv1h6KXLcpMMM9OVFR/R4AAABaVmGv/AAAAQD
+AEgwRgIhANeTA7H51SZUmcT2ldtumFYX6/OkOr0fdvze72U0j9U9AiEAjSOSVQmi
+ZdYK6u3JYkDVOWsEzyKwjPWod8UN5K3ej0EwDQYJKoZIhvcNAQELBQADggEBAJev
+ArtxZVVTmLghV0O7471J1mN1fVC2p6b3AsK/TqrI7aiq8XuQq76KmUsB+U05MTXH
+3sYiHm+/RJ7+ljiKVIC8ZfbQsHo5I+F1CNMo6JB6z8Z+bOeRkoves5FNYmiJnUjO
+uoGzt//CyldbX1dEPVNuU7P0s2wZ6Bubump2LoapGIiGxQJfeb0vj0TQzfRacTIZ
+x9U5E/D0y0iewX4kPHK17QDBsSL9WlqsRzFAkQjJ9XWUVn3BO7JG3WU47iOuykby
+y2HmOWUxjv1Yf/H/OYRBiuSCR4LhrE5Ze4tTo2AByrXQ5h7ezjDJQqnKBP5NuwIq
+7NuX+D2esUNos/D6uJg=
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIEkjCCA3qgAwIBAgIQCgFBQgAAAVOFc2oLheynCDANBgkqhkiG9w0BAQsFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTE2MDMxNzE2NDA0NloXDTIxMDMxNzE2NDA0Nlow
+SjELMAkGA1UEBhMCVVMxFjAUBgNVBAoTDUxldCdzIEVuY3J5cHQxIzAhBgNVBAMT
+GkxldCdzIEVuY3J5cHQgQXV0aG9yaXR5IFgzMIIBIjANBgkqhkiG9w0BAQEFAAOC
+AQ8AMIIBCgKCAQEAnNMM8FrlLke3cl03g7NoYzDq1zUmGSXhvb418XCSL7e4S0EF
+q6meNQhY7LEqxGiHC6PjdeTm86dicbp5gWAf15Gan/PQeGdxyGkOlZHP/uaZ6WA8
+SMx+yk13EiSdRxta67nsHjcAHJyse6cF6s5K671B5TaYucv9bTyWaN8jKkKQDIZ0
+Z8h/pZq4UmEUEz9l6YKHy9v6Dlb2honzhT+Xhq+w3Brvaw2VFn3EK6BlspkENnWA
+a6xK8xuQSXgvopZPKiAlKQTGdMDQMc2PMTiVFrqoM7hD8bEfwzB/onkxEz0tNvjj
+/PIzark5McWvxI0NHWQWM6r6hCm21AvA2H3DkwIDAQABo4IBfTCCAXkwEgYDVR0T
+AQH/BAgwBgEB/wIBADAOBgNVHQ8BAf8EBAMCAYYwfwYIKwYBBQUHAQEEczBxMDIG
+CCsGAQUFBzABhiZodHRwOi8vaXNyZy50cnVzdGlkLm9jc3AuaWRlbnRydXN0LmNv
+bTA7BggrBgEFBQcwAoYvaHR0cDovL2FwcHMuaWRlbnRydXN0LmNvbS9yb290cy9k
+c3Ryb290Y2F4My5wN2MwHwYDVR0jBBgwFoAUxKexpHsscfrb4UuQdf/EFWCFiRAw
+VAYDVR0gBE0wSzAIBgZngQwBAgEwPwYLKwYBBAGC3xMBAQEwMDAuBggrBgEFBQcC
+ARYiaHR0cDovL2Nwcy5yb290LXgxLmxldHNlbmNyeXB0Lm9yZzA8BgNVHR8ENTAz
+MDGgL6AthitodHRwOi8vY3JsLmlkZW50cnVzdC5jb20vRFNUUk9PVENBWDNDUkwu
+Y3JsMB0GA1UdDgQWBBSoSmpjBH3duubRObemRWXv86jsoTANBgkqhkiG9w0BAQsF
+AAOCAQEA3TPXEfNjWDjdGBX7CVW+dla5cEilaUcne8IkCJLxWh9KEik3JHRRHGJo
+uM2VcGfl96S8TihRzZvoroed6ti6WqEBmtzw3Wodatg+VyOeph4EYpr/1wXKtx8/
+wApIvJSwtmVi4MFU5aMqrSDE6ea73Mj2tcMyo5jMd6jmeWUHK8so/joWUoHOUgwu
+X4Po1QYz+3dszkDqMp4fklxBwXRsW10KXzPMTZ+sOPAveyxindmjkW8lGy+QsRlG
+PfZ+G6Z6h7mjem0Y+iWlkYcV4PIWL1iwBi8saCbGS5jN2p8M+X+Q7UNKEkROb3N6
+KOqkqm57TH2H3eDJAkSnh6/DNFu0Qg==
+-----END CERTIFICATE-----
+-----BEGIN CERTIFICATE-----
+MIIDSjCCAjKgAwIBAgIQRK+wgNajJ7qJMDmGLvhAazANBgkqhkiG9w0BAQUFADA/
+MSQwIgYDVQQKExtEaWdpdGFsIFNpZ25hdHVyZSBUcnVzdCBDby4xFzAVBgNVBAMT
+DkRTVCBSb290IENBIFgzMB4XDTAwMDkzMDIxMTIxOVoXDTIxMDkzMDE0MDExNVow
+PzEkMCIGA1UEChMbRGlnaXRhbCBTaWduYXR1cmUgVHJ1c3QgQ28uMRcwFQYDVQQD
+Ew5EU1QgUm9vdCBDQSBYMzCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEB
+AN+v6ZdQCINXtMxiZfaQguzH0yxrMMpb7NnDfcdAwRgUi+DoM3ZJKuM/IUmTrE4O
+rz5Iy2Xu/NMhD2XSKtkyj4zl93ewEnu1lcCJo6m67XMuegwGMoOifooUMM0RoOEq
+OLl5CjH9UL2AZd+3UWODyOKIYepLYYHsUmu5ouJLGiifSKOeDNoJjj4XLh7dIN9b
+xiqKqy69cK3FCxolkHRyxXtqqzTWMIn/5WgTe1QLyNau7Fqckh49ZLOMxt+/yUFw
+7BZy1SbsOFU5Q9D8/RhcQPGX69Wam40dutolucbY38EVAjqr2m7xPi71XAicPNaD
+aeQQmxkqtilX4+U9m5/wAl0CAwEAAaNCMEAwDwYDVR0TAQH/BAUwAwEB/zAOBgNV
+HQ8BAf8EBAMCAQYwHQYDVR0OBBYEFMSnsaR7LHH62+FLkHX/xBVghYkQMA0GCSqG
+SIb3DQEBBQUAA4IBAQCjGiybFwBcqR7uKGY3Or+Dxz9LwwmglSBd49lZRNI+DT69
+ikugdB/OEIKcdBodfpga3csTS7MgROSR6cz8faXbauX+5v3gTt23ADq1cEmv8uXr
+AvHRAosZy5Q6XkjEGB5YGV8eAlrwDPGxrancWYaLbumR9YbK+rlmM6pZW87ipxZz
+R8srzJmwN0jP41ZL9c8PDHIyh8bwRLtTcm1D9SZImlJnt1ir/md2cXjbDaJWFBM5
+JDGFoqgCWjBH4d1QB7wCCZAA62RjYJsWvIjJEubSfZGL+T0yjWW06XyxV3bqxbYo
+Ob8VZRzI9neWagqNdwvYkQsEjgfbKbYK7p2CNTUQ
+-----END CERTIFICATE-----'
+ end
+
trait :with_expired_certificate do
certificate '-----BEGIN CERTIFICATE-----
MIIBsDCCARmgAwIBAgIBATANBgkqhkiG9w0BAQUFADAeMRwwGgYDVQQDExNleHBp
diff --git a/spec/features/admin/admin_groups_spec.rb b/spec/features/admin/admin_groups_spec.rb
index 735ca60f7da..a08902c30be 100644
--- a/spec/features/admin/admin_groups_spec.rb
+++ b/spec/features/admin/admin_groups_spec.rb
@@ -90,6 +90,7 @@ describe 'Admin Groups' do
visit admin_group_path(group)
expect(page).to have_content("Group: #{group.name}")
+ expect(page).to have_content("ID: #{group.id}")
end
end
@@ -102,6 +103,14 @@ describe 'Admin Groups' do
expect_selected_visibility(group.visibility_level)
end
+ it 'shows the subgroup creation level dropdown populated with the group subgroup_creation_level value' do
+ group = create(:group, :private, :owner_subgroup_creation_only)
+
+ visit admin_group_edit_path(group)
+
+ expect(page).to have_content('Allowed to create subgroups')
+ end
+
it 'edit group path does not change group name', :js do
group = create(:group, :private)
diff --git a/spec/features/admin/admin_projects_spec.rb b/spec/features/admin/admin_projects_spec.rb
index 2b6bfa40beb..1c1ca41f633 100644
--- a/spec/features/admin/admin_projects_spec.rb
+++ b/spec/features/admin/admin_projects_spec.rb
@@ -60,6 +60,7 @@ describe "Admin::Projects" do
expect(page).to have_content(project.name)
expect(page).to have_content(project.full_name)
expect(page).to have_content(project.creator.name)
+ expect(page).to have_content(project.id)
end
end
diff --git a/spec/features/groups/group_settings_spec.rb b/spec/features/groups/group_settings_spec.rb
index 5cef5f0521f..676769c25fe 100644
--- a/spec/features/groups/group_settings_spec.rb
+++ b/spec/features/groups/group_settings_spec.rb
@@ -85,6 +85,14 @@ describe 'Edit group settings' do
end
end
+ describe 'subgroup creation level menu' do
+ it 'shows the selection menu' do
+ visit edit_group_path(group)
+
+ expect(page).to have_content('Allowed to create subgroups')
+ end
+ end
+
describe 'edit group avatar' do
before do
visit edit_group_path(group)
diff --git a/spec/features/groups/show_spec.rb b/spec/features/groups/show_spec.rb
index 9671a4d8c49..bed998a0859 100644
--- a/spec/features/groups/show_spec.rb
+++ b/spec/features/groups/show_spec.rb
@@ -56,32 +56,103 @@ describe 'Group show page' do
end
context 'subgroup support' do
- let(:user) { create(:user) }
+ let(:restricted_group) do
+ create(:group, subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
+ end
- before do
- group.add_owner(user)
- sign_in(user)
+ let(:relaxed_group) do
+ create(:group, subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
end
- context 'when subgroups are supported', :js, :nested_groups do
+ let(:owner) { create(:user) }
+ let(:maintainer) { create(:user) }
+
+ context 'for owners' do
+ let(:path) { group_path(restricted_group) }
+
before do
- allow(Group).to receive(:supports_nested_objects?) { true }
- visit path
+ restricted_group.add_owner(owner)
+ sign_in(owner)
end
- it 'allows creating subgroups' do
- expect(page).to have_css("li[data-text='New subgroup']", visible: false)
+ context 'when subgroups are supported', :nested_groups do
+ before do
+ allow(Group).to receive(:supports_nested_objects?) { true }
+ end
+
+ it 'allows creating subgroups' do
+ visit path
+
+ expect(page)
+ .to have_css("li[data-text='New subgroup']", visible: false)
+ end
+ end
+
+ context 'when subgroups are not supported' do
+ before do
+ allow(Group).to receive(:supports_nested_objects?) { false }
+ end
+
+ it 'does not allow creating subgroups' do
+ visit path
+
+ expect(page)
+ .not_to have_selector("li[data-text='New subgroup']", visible: false)
+ end
end
end
- context 'when subgroups are not supported' do
+ context 'for maintainers' do
before do
- allow(Group).to receive(:supports_nested_objects?) { false }
- visit path
+ sign_in(maintainer)
end
- it 'allows creating subgroups' do
- expect(page).not_to have_selector("li[data-text='New subgroup']", visible: false)
+ context 'when subgroups are supported', :nested_groups do
+ before do
+ allow(Group).to receive(:supports_nested_objects?) { true }
+ end
+
+ context 'when subgroup_creation_level is set to maintainers' do
+ before do
+ relaxed_group.add_maintainer(maintainer)
+ end
+
+ it 'allows creating subgroups' do
+ path = group_path(relaxed_group)
+ visit path
+
+ expect(page)
+ .to have_css("li[data-text='New subgroup']", visible: false)
+ end
+ end
+
+ context 'when subgroup_creation_level is set to owners' do
+ before do
+ restricted_group.add_maintainer(maintainer)
+ end
+
+ it 'does not allow creating subgroups' do
+ path = group_path(restricted_group)
+ visit path
+
+ expect(page)
+ .not_to have_selector("li[data-text='New subgroup']",
+ visible: false)
+ end
+ end
+ end
+
+ context 'when subgroups are not supported' do
+ before do
+ allow(Group).to receive(:supports_nested_objects?) { false }
+ end
+
+ it 'does not allow creating subgroups' do
+ visit path
+
+ expect(page)
+ .not_to have_selector("li[data-text='New subgroup']", visible: false)
+ end
end
end
end
diff --git a/spec/features/projects/tree/create_directory_spec.rb b/spec/features/projects/tree/create_directory_spec.rb
index 2cb2a23b7be..8585e24bc35 100644
--- a/spec/features/projects/tree/create_directory_spec.rb
+++ b/spec/features/projects/tree/create_directory_spec.rb
@@ -47,7 +47,9 @@ describe 'Multi-file editor new directory', :js do
fill_in('commit-message', with: 'commit message ide')
- click_button('Commit')
+ page.within '.multi-file-commit-form' do
+ click_button('Commit')
+ end
find('.js-ide-edit-mode').click
diff --git a/spec/features/projects/tree/create_file_spec.rb b/spec/features/projects/tree/create_file_spec.rb
index 9f5524da8e9..8623b10562d 100644
--- a/spec/features/projects/tree/create_file_spec.rb
+++ b/spec/features/projects/tree/create_file_spec.rb
@@ -39,7 +39,9 @@ describe 'Multi-file editor new file', :js do
fill_in('commit-message', with: 'commit message ide')
- click_button('Commit')
+ page.within '.multi-file-commit-form' do
+ click_button('Commit')
+ end
expect(page).to have_content('file name')
end
diff --git a/spec/finders/autocomplete/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb
index c3bc410a7f6..4a87b47bd08 100644
--- a/spec/finders/autocomplete/move_to_project_finder_spec.rb
+++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb
@@ -6,9 +6,9 @@ describe Autocomplete::MoveToProjectFinder do
let(:no_access_project) { create(:project) }
let(:guest_project) { create(:project) }
- let(:reporter_project) { create(:project) }
- let(:developer_project) { create(:project) }
- let(:maintainer_project) { create(:project) }
+ let(:reporter_project) { create(:project, name: 'name') }
+ let(:developer_project) { create(:project, name: 'name2') }
+ let(:maintainer_project) { create(:project, name: 'name3') }
describe '#execute' do
context 'filter' do
@@ -20,14 +20,14 @@ describe Autocomplete::MoveToProjectFinder do
expect(finder.execute).to be_empty
end
- it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
+ it 'returns projects equal or above Gitlab::Access::REPORTER ordered by name' do
reporter_project.add_reporter(user)
developer_project.add_developer(user)
maintainer_project.add_maintainer(user)
finder = described_class.new(user, project_id: project.id)
- expect(finder.execute.to_a).to eq([maintainer_project, developer_project, reporter_project])
+ expect(finder.execute.to_a).to eq([reporter_project, developer_project, maintainer_project])
end
it 'does not include the source project' do
@@ -60,46 +60,32 @@ describe Autocomplete::MoveToProjectFinder do
expect(finder.execute.to_a).to eq([other_reporter_project])
end
- it 'returns a page of projects ordered by id in descending order' do
- allow(Kaminari.config).to receive(:default_per_page).and_return(2)
+ it 'returns a page of projects ordered by name' do
+ stub_const('Autocomplete::MoveToProjectFinder::LIMIT', 2)
- projects = create_list(:project, 2) do |project|
+ projects = create_list(:project, 3) do |project|
project.add_developer(user)
end
finder = described_class.new(user, project_id: project.id)
page = finder.execute.to_a
- expect(page.length).to eq(Kaminari.config.default_per_page)
- expect(page[0]).to eq(projects.last)
- end
-
- it 'returns projects after the given offset id' do
- reporter_project.add_reporter(user)
- developer_project.add_developer(user)
- maintainer_project.add_maintainer(user)
-
- expect(described_class.new(user, project_id: project.id, offset_id: maintainer_project.id).execute.to_a)
- .to eq([developer_project, reporter_project])
-
- expect(described_class.new(user, project_id: project.id, offset_id: developer_project.id).execute.to_a)
- .to eq([reporter_project])
-
- expect(described_class.new(user, project_id: project.id, offset_id: reporter_project.id).execute.to_a)
- .to be_empty
+ expected_projects = projects.sort_by(&:name).first(2)
+ expect(page.length).to eq(2)
+ expect(page).to eq(expected_projects)
end
end
context 'search' do
it 'returns projects matching a search query' do
- foo_project = create(:project)
+ foo_project = create(:project, name: 'foo')
foo_project.add_maintainer(user)
wadus_project = create(:project, name: 'wadus')
wadus_project.add_maintainer(user)
expect(described_class.new(user, project_id: project.id).execute.to_a)
- .to eq([wadus_project, foo_project])
+ .to eq([foo_project, wadus_project])
expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
.to eq([wadus_project])
diff --git a/spec/frontend/behaviors/markdown/render_metrics_spec.js b/spec/frontend/behaviors/markdown/render_metrics_spec.js
new file mode 100644
index 00000000000..6db0eabc16b
--- /dev/null
+++ b/spec/frontend/behaviors/markdown/render_metrics_spec.js
@@ -0,0 +1,37 @@
+import Vue from 'vue';
+import renderMetrics from '~/behaviors/markdown/render_metrics';
+import { TEST_HOST } from 'helpers/test_constants';
+
+const originalExtend = Vue.extend;
+
+describe('Render metrics for Gitlab Flavoured Markdown', () => {
+ const container = {
+ Metrics() {},
+ };
+
+ let spyExtend;
+
+ beforeEach(() => {
+ Vue.extend = () => container.Metrics;
+ spyExtend = jest.spyOn(Vue, 'extend');
+ });
+
+ afterEach(() => {
+ Vue.extend = originalExtend;
+ });
+
+ it('does nothing when no elements are found', () => {
+ renderMetrics([]);
+
+ expect(spyExtend).not.toHaveBeenCalled();
+ });
+
+ it('renders a vue component when elements are found', () => {
+ const element = document.createElement('div');
+ element.setAttribute('data-dashboard-url', TEST_HOST);
+
+ renderMetrics([element]);
+
+ expect(spyExtend).toHaveBeenCalled();
+ });
+});
diff --git a/spec/frontend/monitoring/embed/embed_spec.js b/spec/frontend/monitoring/embed/embed_spec.js
new file mode 100644
index 00000000000..3b18a0f77c7
--- /dev/null
+++ b/spec/frontend/monitoring/embed/embed_spec.js
@@ -0,0 +1,78 @@
+import { createLocalVue, shallowMount } from '@vue/test-utils';
+import Vuex from 'vuex';
+import Embed from '~/monitoring/components/embed.vue';
+import MonitorAreaChart from '~/monitoring/components/charts/area.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+import { groups, initialState, metricsData, metricsWithData } from './mock_data';
+
+const localVue = createLocalVue();
+localVue.use(Vuex);
+
+describe('Embed', () => {
+ let wrapper;
+ let store;
+ let actions;
+
+ function mountComponent() {
+ wrapper = shallowMount(Embed, {
+ localVue,
+ store,
+ propsData: {
+ dashboardUrl: TEST_HOST,
+ },
+ });
+ }
+
+ beforeEach(() => {
+ actions = {
+ setFeatureFlags: () => {},
+ setShowErrorBanner: () => {},
+ setEndpoints: () => {},
+ fetchMetricsData: () => {},
+ };
+
+ store = new Vuex.Store({
+ modules: {
+ monitoringDashboard: {
+ namespaced: true,
+ actions,
+ state: initialState,
+ },
+ },
+ });
+ });
+
+ afterEach(() => {
+ if (wrapper) {
+ wrapper.destroy();
+ }
+ });
+
+ describe('no metrics are available yet', () => {
+ beforeEach(() => {
+ mountComponent();
+ });
+
+ it('shows an empty state when no metrics are present', () => {
+ expect(wrapper.find('.metrics-embed').exists()).toBe(true);
+ expect(wrapper.find(MonitorAreaChart).exists()).toBe(false);
+ });
+ });
+
+ describe('metrics are available', () => {
+ beforeEach(() => {
+ store.state.monitoringDashboard.groups = groups;
+ store.state.monitoringDashboard.groups[0].metrics = metricsData;
+ store.state.monitoringDashboard.metricsWithData = metricsWithData;
+
+ mountComponent();
+ });
+
+ it('shows a chart when metrics are present', () => {
+ wrapper.setProps({});
+ expect(wrapper.find('.metrics-embed').exists()).toBe(true);
+ expect(wrapper.find(MonitorAreaChart).exists()).toBe(true);
+ expect(wrapper.findAll(MonitorAreaChart).length).toBe(2);
+ });
+ });
+});
diff --git a/spec/frontend/monitoring/embed/mock_data.js b/spec/frontend/monitoring/embed/mock_data.js
new file mode 100644
index 00000000000..df4acb82e95
--- /dev/null
+++ b/spec/frontend/monitoring/embed/mock_data.js
@@ -0,0 +1,87 @@
+export const metricsWithData = [15, 16];
+
+export const groups = [
+ {
+ panels: [
+ {
+ title: 'Memory Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Memory Used',
+ weight: 4,
+ metrics: [
+ {
+ id: 'system_metrics_kubernetes_container_memory_total',
+ metric_id: 15,
+ },
+ ],
+ },
+ {
+ title: 'Core Usage (Total)',
+ type: 'area-chart',
+ y_label: 'Total Cores',
+ weight: 3,
+ metrics: [
+ {
+ id: 'system_metrics_kubernetes_container_cores_total',
+ metric_id: 16,
+ },
+ ],
+ },
+ ],
+ },
+];
+
+export const metrics = [
+ {
+ id: 'system_metrics_kubernetes_container_memory_total',
+ metric_id: 15,
+ },
+ {
+ id: 'system_metrics_kubernetes_container_cores_total',
+ metric_id: 16,
+ },
+];
+
+const queries = [
+ {
+ result: [
+ {
+ values: [
+ ['Mon', 1220],
+ ['Tue', 932],
+ ['Wed', 901],
+ ['Thu', 934],
+ ['Fri', 1290],
+ ['Sat', 1330],
+ ['Sun', 1320],
+ ],
+ },
+ ],
+ },
+];
+
+export const metricsData = [
+ {
+ queries,
+ metrics: [
+ {
+ metric_id: 15,
+ },
+ ],
+ },
+ {
+ queries,
+ metrics: [
+ {
+ metric_id: 16,
+ },
+ ],
+ },
+];
+
+export const initialState = {
+ monitoringDashboard: {},
+ groups: [],
+ metricsWithData: [],
+ useDashboardEndpoint: true,
+};
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index 634c78ec029..e4d62b044ca 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -69,3 +69,9 @@ Object.entries(jqueryMatchers).forEach(([matcherName, matcherFactory]) => {
// Tech debt issue TBD
testUtilsConfig.logModifiedComponents = false;
+
+// Basic stub for MutationObserver
+global.MutationObserver = () => ({
+ disconnect: () => {},
+ observe: () => {},
+});
diff --git a/spec/helpers/submodule_helper_spec.rb b/spec/helpers/submodule_helper_spec.rb
index ea48c69e0ae..b922b910c89 100644
--- a/spec/helpers/submodule_helper_spec.rb
+++ b/spec/helpers/submodule_helper_spec.rb
@@ -3,15 +3,11 @@ require 'spec_helper'
describe SubmoduleHelper do
include RepoHelpers
- describe 'submodule links' do
- let(:submodule_item) { double(id: 'hash', path: 'rack') }
- let(:config) { Gitlab.config.gitlab }
- let(:repo) { double }
-
- before do
- self.instance_variable_set(:@repository, repo)
- end
+ let(:submodule_item) { double(id: 'hash', path: 'rack') }
+ let(:config) { Gitlab.config.gitlab }
+ let(:repo) { double }
+ shared_examples 'submodule_links' do
context 'submodule on self' do
before do
allow(Gitlab.config.gitlab).to receive(:protocol).and_return('http') # set this just to be sure
@@ -21,28 +17,28 @@ describe SubmoduleHelper do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(22) # set this just to be sure
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url([config.user, '@', config.host, ':gitlab-org/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects ssh on non-standard port' do
allow(Gitlab.config.gitlab_shell).to receive(:ssh_port).and_return(2222)
allow(Gitlab.config.gitlab_shell).to receive(:ssh_path_prefix).and_return(Settings.send(:build_gitlab_shell_ssh_path_prefix))
stub_url(['ssh://', config.user, '@', config.host, ':2222/gitlab-org/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(80)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url(['http://', config.host, '/gitlab-org/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'detects http on non-standard port' do
allow(Gitlab.config.gitlab).to receive(:port).and_return(3000)
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url(['http://', config.host, ':3000/gitlab-org/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'works with relative_url_root' do
@@ -50,7 +46,7 @@ describe SubmoduleHelper do
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url(['http://', config.host, '/gitlab/root/gitlab-org/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org', 'gitlab-ce'), namespace_project_tree_path('gitlab-org', 'gitlab-ce', 'hash')])
end
it 'works with subgroups' do
@@ -58,34 +54,34 @@ describe SubmoduleHelper do
allow(Gitlab.config.gitlab).to receive(:relative_url_root).and_return('/gitlab/root')
allow(Gitlab.config.gitlab).to receive(:url).and_return(Settings.send(:build_gitlab_url))
stub_url(['http://', config.host, '/gitlab/root/gitlab-org/sub/gitlab-ce.git'].join(''))
- expect(submodule_links(submodule_item)).to eq([namespace_project_path('gitlab-org/sub', 'gitlab-ce'), namespace_project_tree_path('gitlab-org/sub', 'gitlab-ce', 'hash')])
+ expect(subject).to eq([namespace_project_path('gitlab-org/sub', 'gitlab-ce'), namespace_project_tree_path('gitlab-org/sub', 'gitlab-ce', 'hash')])
end
end
context 'submodule on github.com' do
it 'detects ssh' do
stub_url('git@github.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://github.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'handles urls with no .git on the end' do
stub_url('http://github.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://github.com/gitlab-org/gitlab-ce', 'https://github.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://github.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
+ expect(subject).to eq([repo.submodule_url_for, nil])
end
end
@@ -97,39 +93,39 @@ describe SubmoduleHelper do
allow(repo).to receive(:project).and_return(project)
stub_url('./')
- expect(submodule_links(submodule_item)).to eq(["/master-project/#{project.path}", "/master-project/#{project.path}/tree/hash"])
+ expect(subject).to eq(["/master-project/#{project.path}", "/master-project/#{project.path}/tree/hash"])
end
end
context 'submodule on gitlab.com' do
it 'detects ssh' do
stub_url('git@gitlab.com:gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects http' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'detects https' do
stub_url('https://gitlab.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'handles urls with no .git on the end' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'handles urls with trailing whitespace' do
stub_url('http://gitlab.com/gitlab-org/gitlab-ce.git ')
- expect(submodule_links(submodule_item)).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
+ expect(subject).to eq(['https://gitlab.com/gitlab-org/gitlab-ce', 'https://gitlab.com/gitlab-org/gitlab-ce/tree/hash'])
end
it 'returns original with non-standard url' do
stub_url('http://gitlab.com/another/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
+ expect(subject).to eq([repo.submodule_url_for, nil])
end
end
@@ -137,27 +133,25 @@ describe SubmoduleHelper do
it 'sanitizes unsupported protocols' do
stub_url('javascript:alert("XSS");')
- expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ expect(subject).to eq([nil, nil])
end
it 'sanitizes unsupported protocols disguised as a repository URL' do
stub_url('javascript:alert("XSS");foo/bar.git')
- expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ expect(subject).to eq([nil, nil])
end
it 'sanitizes invalid URL with extended ASCII' do
stub_url('é')
- expect(helper.submodule_links(submodule_item)).to eq([nil, nil])
+ expect(subject).to eq([nil, nil])
end
it 'returns original' do
stub_url('http://mygitserver.com/gitlab-org/gitlab-ce')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
- stub_url('http://mygitserver.com/gitlab-org/gitlab-ce.git')
- expect(submodule_links(submodule_item)).to eq([repo.submodule_url_for, nil])
+ expect(subject).to eq([repo.submodule_url_for, nil])
end
end
@@ -168,8 +162,7 @@ describe SubmoduleHelper do
def expect_relative_link_to_resolve_to(relative_path, expected_path)
allow(repo).to receive(:submodule_url_for).and_return(relative_path)
-
- result = submodule_links(submodule_item)
+ result = subject
expect(result).to eq([expected_path, "#{expected_path}/tree/#{submodule_item.id}"])
end
@@ -190,7 +183,7 @@ describe SubmoduleHelper do
it 'returns nil' do
allow(repo).to receive(:submodule_url_for).and_return('../../test.git')
- result = submodule_links(submodule_item)
+ result = subject
expect(result).to eq([nil, nil])
end
@@ -200,7 +193,7 @@ describe SubmoduleHelper do
it 'returns nil because it is not possible to have repo nested under another repo' do
allow(repo).to receive(:submodule_url_for).and_return('./test.git')
- result = submodule_links(submodule_item)
+ result = subject
expect(result).to eq([nil, nil])
end
@@ -238,6 +231,22 @@ describe SubmoduleHelper do
end
end
+ context 'as view helpers in view context' do
+ subject { helper.submodule_links(submodule_item) }
+
+ before do
+ self.instance_variable_set(:@repository, repo)
+ end
+
+ it_behaves_like 'submodule_links'
+ end
+
+ context 'as stand-alone module' do
+ subject { described_class.submodule_links(submodule_item, nil, repo) }
+
+ it_behaves_like 'submodule_links'
+ end
+
def stub_url(url)
allow(repo).to receive(:submodule_url_for).and_return(url)
end
diff --git a/spec/javascripts/helpers/vue_test_utils_helper.js b/spec/javascripts/helpers/vue_test_utils_helper.js
index 121e99c9783..68326e37ae7 100644
--- a/spec/javascripts/helpers/vue_test_utils_helper.js
+++ b/spec/javascripts/helpers/vue_test_utils_helper.js
@@ -1,5 +1,3 @@
-/* eslint-disable import/prefer-default-export */
-
const vNodeContainsText = (vnode, text) =>
(vnode.text && vnode.text.includes(text)) ||
(vnode.children && vnode.children.filter(child => vNodeContainsText(child, text)).length);
@@ -19,3 +17,19 @@ export const shallowWrapperContainsSlotText = (shallowWrapper, slotName, text) =
Boolean(
shallowWrapper.vm.$slots[slotName].filter(vnode => vNodeContainsText(vnode, text)).length,
);
+
+/**
+ * Returns a promise that waits for a mutation to be fired before resolving
+ * NOTE: There's no reject action here so it will hang if it waits for a mutation that won't happen.
+ * @param {Object} store - The Vue store that contains the mutations
+ * @param {String} expectedMutationType - The Mutation to wait for
+ */
+export const waitForMutation = (store, expectedMutationType) =>
+ new Promise(resolve => {
+ const unsubscribe = store.subscribe(mutation => {
+ if (mutation.type === expectedMutationType) {
+ unsubscribe();
+ resolve();
+ }
+ });
+ });
diff --git a/spec/lib/gitlab/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index 7baa52ffb4f..929b6222900 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -343,7 +343,6 @@ project:
- fork_network_member
- fork_network
- custom_attributes
-- prometheus_metrics
- lfs_file_locks
- project_badges
- source_of_merge_requests
diff --git a/spec/models/group_spec.rb b/spec/models/group_spec.rb
index 470ce65707d..c7fb0f51075 100644
--- a/spec/models/group_spec.rb
+++ b/spec/models/group_spec.rb
@@ -994,4 +994,11 @@ describe Group do
expect(group.project_creation_level).to eq(Gitlab::CurrentSettings.default_project_creation)
end
end
+
+ describe 'subgroup_creation_level' do
+ it 'defaults to maintainers' do
+ expect(group.subgroup_creation_level)
+ .to eq(Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ end
+ end
end
diff --git a/spec/models/pages_domain_spec.rb b/spec/models/pages_domain_spec.rb
index 973c67937b7..519c519fbcf 100644
--- a/spec/models/pages_domain_spec.rb
+++ b/spec/models/pages_domain_spec.rb
@@ -127,6 +127,30 @@ describe PagesDomain do
it { is_expected.not_to be_valid }
end
+
+ context 'when certificate is expired' do
+ let(:domain) do
+ build(:pages_domain, :with_trusted_expired_chain)
+ end
+
+ context 'when certificate is being changed' do
+ it "adds error to certificate" do
+ domain.valid?
+
+ expect(domain.errors.keys).to contain_exactly(:key, :certificate)
+ end
+ end
+
+ context 'when certificate is already saved' do
+ it "doesn't add error to certificate" do
+ domain.save(validate: false)
+
+ domain.valid?
+
+ expect(domain.errors.keys).to contain_exactly(:key)
+ end
+ end
+ end
end
describe 'validations' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 927c072be10..bcb2da7eed2 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1675,26 +1675,6 @@ describe Project do
end
end
- describe '.paginate_in_descending_order_using_id' do
- let!(:project1) { create(:project) }
- let!(:project2) { create(:project) }
-
- it 'orders the relation in descending order' do
- expect(described_class.paginate_in_descending_order_using_id)
- .to eq([project2, project1])
- end
-
- it 'applies a limit to the relation' do
- expect(described_class.paginate_in_descending_order_using_id(limit: 1))
- .to eq([project2])
- end
-
- it 'limits projects by and ID when given' do
- expect(described_class.paginate_in_descending_order_using_id(before: project2.id))
- .to eq([project1])
- end
- end
-
describe '.including_namespace_and_owner' do
it 'eager loads the namespace and namespace owner' do
create(:project)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 59f3a961d50..dc3675a7b9e 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -98,12 +98,38 @@ describe GroupPolicy do
context 'maintainer' do
let(:current_user) { maintainer }
- it do
- expect_allowed(*guest_permissions)
- expect_allowed(*reporter_permissions)
- expect_allowed(*developer_permissions)
- expect_allowed(*maintainer_permissions)
- expect_disallowed(*owner_permissions)
+ context 'with subgroup_creation level set to maintainer' do
+ let(:group) do
+ create(:group, :private, subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ end
+
+ it 'allows every maintainer permission plus creating subgroups' do
+ allow(Group).to receive(:supports_nested_objects?).and_return(true)
+
+ create_subgroup_permission = [:create_subgroup]
+ updated_maintainer_permissions =
+ maintainer_permissions + create_subgroup_permission
+ updated_owner_permissions =
+ owner_permissions - create_subgroup_permission
+
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_allowed(*updated_maintainer_permissions)
+ expect_disallowed(*updated_owner_permissions)
+ end
+ end
+
+ context 'with subgroup_creation_level set to owner' do
+ it 'allows every maintainer permission' do
+ allow(Group).to receive(:supports_nested_objects?).and_return(true)
+
+ expect_allowed(*guest_permissions)
+ expect_allowed(*reporter_permissions)
+ expect_allowed(*developer_permissions)
+ expect_allowed(*maintainer_permissions)
+ expect_disallowed(*owner_permissions)
+ end
end
end
@@ -145,7 +171,8 @@ describe GroupPolicy do
it 'allows every owner permission except creating subgroups' do
create_subgroup_permission = [:create_subgroup]
- updated_owner_permissions = owner_permissions - create_subgroup_permission
+ updated_owner_permissions =
+ owner_permissions - create_subgroup_permission
expect_disallowed(*create_subgroup_permission)
expect_allowed(*updated_owner_permissions)
@@ -157,16 +184,32 @@ describe GroupPolicy do
it 'allows every owner permission except creating subgroups' do
create_subgroup_permission = [:create_subgroup]
- updated_owner_permissions = owner_permissions - create_subgroup_permission
+ updated_owner_permissions =
+ owner_permissions - create_subgroup_permission
expect_disallowed(*create_subgroup_permission)
expect_allowed(*updated_owner_permissions)
end
end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it 'allows every maintainer permission except creating subgroups' do
+ create_subgroup_permission = [:create_subgroup]
+ updated_maintainer_permissions =
+ maintainer_permissions - create_subgroup_permission
+
+ expect_disallowed(*create_subgroup_permission)
+ expect_allowed(*updated_maintainer_permissions)
+ end
+ end
end
describe 'private nested group use the highest access level from the group and inherited permissions', :nested_groups do
- let(:nested_group) { create(:group, :private, parent: group) }
+ let(:nested_group) do
+ create(:group, :private, :owner_subgroup_creation_only, parent: group)
+ end
before do
nested_group.add_guest(guest)
@@ -461,6 +504,72 @@ describe GroupPolicy do
end
end
+ context "create_subgroup" do
+ context 'when group has subgroup creation level set to owner' do
+ let(:group) do
+ create(
+ :group,
+ subgroup_creation_level: ::Gitlab::Access::OWNER_SUBGROUP_ACCESS)
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_subgroup) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_subgroup) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_disallowed(:create_subgroup) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_subgroup) }
+ end
+ end
+
+ context 'when group has subgroup creation level set to maintainer' do
+ let(:group) do
+ create(
+ :group,
+ subgroup_creation_level: ::Gitlab::Access::MAINTAINER_SUBGROUP_ACCESS)
+ end
+
+ context 'reporter' do
+ let(:current_user) { reporter }
+
+ it { is_expected.to be_disallowed(:create_subgroup) }
+ end
+
+ context 'developer' do
+ let(:current_user) { developer }
+
+ it { is_expected.to be_disallowed(:create_subgroup) }
+ end
+
+ context 'maintainer' do
+ let(:current_user) { maintainer }
+
+ it { is_expected.to be_allowed(:create_subgroup) }
+ end
+
+ context 'owner' do
+ let(:current_user) { owner }
+
+ it { is_expected.to be_allowed(:create_subgroup) }
+ end
+ end
+ end
+
it_behaves_like 'clusterable policies' do
let(:clusterable) { create(:group) }
let(:cluster) do
diff --git a/spec/requests/api/groups_spec.rb b/spec/requests/api/groups_spec.rb
index c41408fba65..52d926d5484 100644
--- a/spec/requests/api/groups_spec.rb
+++ b/spec/requests/api/groups_spec.rb
@@ -803,10 +803,10 @@ describe API::Groups do
group2.add_maintainer(user1)
end
- it 'cannot create subgroups' do
+ it 'can create subgroups' do
post api("/groups", user1), params: { parent_id: group2.id, name: 'foo', path: 'foo' }
- expect(response).to have_gitlab_http_status(403)
+ expect(response).to have_gitlab_http_status(201)
end
end
end
diff --git a/spec/services/groups/create_service_spec.rb b/spec/services/groups/create_service_spec.rb
index c5ff6cdbacd..a7c95428485 100644
--- a/spec/services/groups/create_service_spec.rb
+++ b/spec/services/groups/create_service_spec.rb
@@ -87,6 +87,14 @@ describe Groups::CreateService, '#execute' do
it { is_expected.to be_persisted }
end
+
+ context 'as maintainer' do
+ before do
+ group.add_maintainer(user)
+ end
+
+ it { is_expected.to be_persisted }
+ end
end
end
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 56ac208a025..60990879fe2 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -58,6 +58,7 @@ Capybara.javascript_driver = :chrome
Capybara.default_max_wait_time = timeout
Capybara.ignore_hidden_elements = true
Capybara.default_normalize_ws = true
+Capybara.enable_aria_label = true
# Keep only the screenshots generated from the last failing test suite
Capybara::Screenshot.prune_strategy = :keep_last_run
diff --git a/spec/support/shared_contexts/policies/group_policy_shared_context.rb b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
index b4808ac0068..74389c4d82b 100644
--- a/spec/support/shared_contexts/policies/group_policy_shared_context.rb
+++ b/spec/support/shared_contexts/policies/group_policy_shared_context.rb
@@ -7,7 +7,7 @@ RSpec.shared_context 'GroupPolicy context' do
let(:maintainer) { create(:user) }
let(:owner) { create(:user) }
let(:admin) { create(:admin) }
- let(:group) { create(:group, :private) }
+ let(:group) { create(:group, :private, :owner_subgroup_creation_only) }
let(:guest_permissions) do
%i[