diff --git a/app/assets/javascripts/vue_shared/components/web_ide_link.vue b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
index df0981aea7..6da2d39a95 100644
--- a/app/assets/javascripts/vue_shared/components/web_ide_link.vue
+++ b/app/assets/javascripts/vue_shared/components/web_ide_link.vue
@@ -92,7 +92,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-edit',
- handle: () => this.showModal('#modal-confirm-fork-edit'),
+ handle: () => {
+ this.$emit('edit', 'simple');
+ this.showModal('#modal-confirm-fork-edit');
+ },
}
: { href: this.editUrl };
@@ -128,7 +131,10 @@ export default {
const handleOptions = this.needsToFork
? {
href: '#modal-confirm-fork-webide',
- handle: () => this.showModal('#modal-confirm-fork-webide'),
+ handle: () => {
+ this.$emit('edit', 'ide');
+ this.showModal('#modal-confirm-fork-webide');
+ },
}
: { href: this.webIdeUrl };
diff --git a/app/assets/javascripts/vue_shared/directives/validation.js b/app/assets/javascripts/vue_shared/directives/validation.js
index 692f2769b8..779b04dc2b 100644
--- a/app/assets/javascripts/vue_shared/directives/validation.js
+++ b/app/assets/javascripts/vue_shared/directives/validation.js
@@ -1,4 +1,3 @@
-import { merge } from 'lodash';
import { s__ } from '~/locale';
/**
@@ -21,8 +20,15 @@ const defaultFeedbackMap = {
},
};
-const getFeedbackForElement = (feedbackMap, el) =>
- Object.values(feedbackMap).find((f) => f.isInvalid(el))?.message || el.validationMessage;
+const getFeedbackForElement = (feedbackMap, el) => {
+ const field = Object.values(feedbackMap).find((f) => f.isInvalid(el));
+ let elMessage = null;
+ if (field) {
+ elMessage = el.getAttribute('validation-message');
+ }
+
+ return field?.message || elMessage || el.validationMessage;
+};
const focusFirstInvalidInput = (e) => {
const { target: formEl } = e;
@@ -68,6 +74,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa
/**
* Takes an object that allows to add or change custom feedback messages.
+ * See possibilities here: https://developer.mozilla.org/en-US/docs/Web/API/ValidityState
*
* The passed in object will be merged with the built-in feedback
* so it is possible to override a built-in message.
@@ -75,7 +82,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa
* @example
* validate({
* tooLong: {
- * check: el => el.validity.tooLong === true,
+ * isInvalid: el => el.validity.tooLong === true,
* message: 'Your custom feedback'
* }
* })
@@ -91,7 +98,7 @@ const createValidator = (context, feedbackMap) => ({ el, reportInvalidInput = fa
* @returns {{ inserted: function, update: function }} validateDirective
*/
export default function initValidation(customFeedbackMap = {}) {
- const feedbackMap = merge(defaultFeedbackMap, customFeedbackMap);
+ const feedbackMap = { ...defaultFeedbackMap, ...customFeedbackMap };
const elDataMap = new WeakMap();
return {
diff --git a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
index 0ff858e6af..42272c222f 100644
--- a/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
+++ b/app/assets/javascripts/vue_shared/security_configuration/components/manage_via_mr.vue
@@ -100,6 +100,7 @@ export default {
:loading="isLoading"
:variant="variant"
:category="category"
+ :data-qa-selector="`${feature.type}_mr_button`"
@click="mutate"
>{{ $options.i18n.buttonLabel }}
diff --git a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
index ad40ea6a96..12f2bc7150 100644
--- a/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
+++ b/app/assets/javascripts/vue_shared/security_reports/security_reports_app.vue
@@ -50,7 +50,7 @@ export default {
required: false,
default: '',
},
- secretScanningComparisonPath: {
+ secretDetectionComparisonPath: {
type: String,
required: false,
default: '',
@@ -149,8 +149,8 @@ export default {
this.canShowCounts = true;
}
- if (this.secretScanningComparisonPath && this.hasSecretDetectionReports) {
- this.setSecretDetectionDiffEndpoint(this.secretScanningComparisonPath);
+ if (this.secretDetectionComparisonPath && this.hasSecretDetectionReports) {
+ this.setSecretDetectionDiffEndpoint(this.secretDetectionComparisonPath);
this.fetchSecretDetectionDiff();
this.canShowCounts = true;
}
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
index 4f92e181f9..62a51abe03 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/sast/actions.js
@@ -1,3 +1,4 @@
+import { REPORT_TYPE_SAST } from '~/vue_shared/security_reports/constants';
import { fetchDiffData } from '../../utils';
import * as types from './mutation_types';
@@ -14,7 +15,7 @@ export const receiveDiffError = ({ commit }, response) =>
export const fetchDiff = ({ state, rootState, dispatch }) => {
dispatch('requestDiff');
- return fetchDiffData(rootState, state.paths.diffEndpoint, 'sast')
+ return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SAST)
.then((data) => {
dispatch('receiveDiffSuccess', data);
})
diff --git a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
index e3ae5435f5..722dcce307 100644
--- a/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
+++ b/app/assets/javascripts/vue_shared/security_reports/store/modules/secret_detection/actions.js
@@ -1,3 +1,4 @@
+import { REPORT_TYPE_SECRET_DETECTION } from '~/vue_shared/security_reports/constants';
import { fetchDiffData } from '../../utils';
import * as types from './mutation_types';
@@ -14,7 +15,7 @@ export const receiveDiffError = ({ commit }, response) =>
export const fetchDiff = ({ state, rootState, dispatch }) => {
dispatch('requestDiff');
- return fetchDiffData(rootState, state.paths.diffEndpoint, 'secret_detection')
+ return fetchDiffData(rootState, state.paths.diffEndpoint, REPORT_TYPE_SECRET_DETECTION)
.then((data) => {
dispatch('receiveDiffSuccess', data);
})
diff --git a/app/assets/stylesheets/_page_specific_files.scss b/app/assets/stylesheets/_page_specific_files.scss
index fa5ab59023..8f3b5b3b7c 100644
--- a/app/assets/stylesheets/_page_specific_files.scss
+++ b/app/assets/stylesheets/_page_specific_files.scss
@@ -1,5 +1,4 @@
@import './pages/branches';
-@import './pages/ci_projects';
@import './pages/clusters';
@import './pages/commits';
@import './pages/deploy_keys';
@@ -10,7 +9,6 @@
@import './pages/groups';
@import './pages/help';
@import './pages/issuable';
-@import './pages/issues/issue_count_badge';
@import './pages/issues';
@import './pages/labels';
@import './pages/login';
diff --git a/app/assets/stylesheets/application_dark.scss b/app/assets/stylesheets/application_dark.scss
index dae0cd72a8..f1d7df8c5e 100644
--- a/app/assets/stylesheets/application_dark.scss
+++ b/app/assets/stylesheets/application_dark.scss
@@ -2,74 +2,4 @@
@import './application';
-@import './themes/theme_helper';
-
-body.gl-dark {
- @include gitlab-theme(
- $gray-900,
- $gray-400,
- $gray-500,
- $gray-800,
- $gray-900,
- $white
- );
-
- .logo-text svg {
- fill: var(--gl-text-color);
- }
-
- .navbar-gitlab {
- background-color: var(--gray-50);
- box-shadow: 0 1px 0 0 var(--gray-100);
-
- .navbar-sub-nav,
- .navbar-nav {
- li {
- > a:hover,
- > a:focus,
- > button:hover,
- > button:focus {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
- }
- }
-
- li.active,
- li.dropdown.show {
- > a,
- > button {
- color: var(--gl-text-color);
- background-color: var(--gray-200);
- }
- }
- }
-
- .header-search {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--border-color) !important;
-
- &:active,
- &:hover {
- background-color: var(--gray-100) !important;
- box-shadow: inset 0 0 0 1px var(--blue-200) !important;
- }
- }
-
- .search {
- form {
- background-color: var(--gray-100);
- box-shadow: inset 0 0 0 1px var(--border-color);
-
- &:active,
- &:hover {
- background-color: var(--gray-100);
- box-shadow: inset 0 0 0 1px var(--blue-200);
- }
- }
- }
- }
-
- .md :not(pre.code) > code {
- background-color: $gray-200;
- }
-}
+@import './themes/dark_mode_overrides';
diff --git a/app/assets/stylesheets/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index c4f292dd05..27ddff181c 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -112,7 +112,7 @@ code {
border-radius: $border-radius-default;
.code > &,
- .build-trace & {
+ .build-log & {
background-color: inherit;
padding: unset;
}
diff --git a/app/assets/stylesheets/components/batch_comments/review_bar.scss b/app/assets/stylesheets/components/batch_comments/review_bar.scss
index bcd0697481..6f5c5c5a08 100644
--- a/app/assets/stylesheets/components/batch_comments/review_bar.scss
+++ b/app/assets/stylesheets/components/batch_comments/review_bar.scss
@@ -38,59 +38,6 @@
margin: 0 auto;
}
-.review-preview-dropdown {
- .review-preview-item.menu-item {
- display: flex;
- flex-wrap: wrap;
- padding: 8px 16px;
- cursor: pointer;
-
- &:not(.is-last) {
- border-bottom: 1px solid $list-border;
- }
- }
-
- .dropdown-menu {
- top: auto;
- bottom: 36px;
-
- &.show {
- max-height: 400px;
-
- @include media-breakpoint-down(xs) {
- width: calc(100vw - 32px);
- }
- }
- }
-
- .dropdown-content {
- max-height: 300px;
- }
-
- .dropdown-title {
- padding: $grid-size 25px $gl-padding;
- margin-bottom: 0;
- }
-
- .dropdown-footer {
- margin-top: 0;
- }
-
- .dropdown-menu-close {
- top: 6px;
- }
-}
-
-.review-preview-dropdown-toggle {
- svg.s16 {
- width: 15px;
- height: 15px;
- margin-top: -1px;
- top: 3px;
- margin-left: 4px;
- }
-}
-
.review-preview-item-header {
display: flex;
align-items: center;
diff --git a/app/assets/stylesheets/components/content_editor.scss b/app/assets/stylesheets/components/content_editor.scss
index a013d971ef..7f498b79d3 100644
--- a/app/assets/stylesheets/components/content_editor.scss
+++ b/app/assets/stylesheets/components/content_editor.scss
@@ -1,9 +1,13 @@
.ProseMirror {
+ max-height: 55vh;
+ overflow-y: auto;
+
td,
th,
li,
dd,
- dt {
+ dt,
+ summary {
:first-child {
margin-bottom: 0 !important;
}
@@ -37,6 +41,7 @@
}
}
+
.dl-content {
width: 100%;
@@ -50,6 +55,38 @@
}
}
}
+
+ .details-toggle-icon {
+ cursor: pointer;
+ z-index: 1;
+
+ &::before {
+ content: '▶';
+ display: inline-block;
+ width: $gl-spacing-scale-4;
+ }
+
+ &.is-open::before {
+ content: '▼';
+ }
+ }
+
+ .details-content {
+ width: calc(100% - #{$gl-spacing-scale-4});
+
+ > li {
+ list-style-type: none;
+ margin-left: $gl-spacing-scale-2;
+ }
+
+ > :not(:first-child) {
+ display: none;
+ }
+
+ &.is-open > :not(:first-child) {
+ display: inherit;
+ }
+ }
}
.table-creator-grid-item {
@@ -70,3 +107,17 @@
@include gl-white-space-nowrap;
}
+
+
+.content-editor-color-chip::after {
+ content: ' ';
+ display: inline-block;
+ align-items: center;
+ width: 11px;
+ height: 11px;
+ border-radius: 3px;
+ margin-left: 4px;
+ margin-top: -2px;
+ border: 1px solid $black-transparent;
+ background-color: var(--gl-color-chip-color);
+}
diff --git a/app/assets/stylesheets/components/design_management/design.scss b/app/assets/stylesheets/components/design_management/design.scss
index 579a86a94a..a3cbdb9ae8 100644
--- a/app/assets/stylesheets/components/design_management/design.scss
+++ b/app/assets/stylesheets/components/design_management/design.scss
@@ -70,11 +70,6 @@ $t-gray-a-16-design-pin: rgba($black, 0.16);
}
}
-.design-presentation-wrapper {
- top: 0;
- left: 0;
-}
-
.design-scaler-wrapper {
bottom: 0;
left: 50%;
diff --git a/app/assets/stylesheets/components/design_management/design_version_dropdown.scss b/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
deleted file mode 100644
index f79d672e23..0000000000
--- a/app/assets/stylesheets/components/design_management/design_version_dropdown.scss
+++ /dev/null
@@ -1,3 +0,0 @@
-.design-version-dropdown > button {
- background: inherit;
-}
diff --git a/app/assets/stylesheets/components/project_list_item.scss b/app/assets/stylesheets/components/project_list_item.scss
deleted file mode 100644
index 8e7c2c4398..0000000000
--- a/app/assets/stylesheets/components/project_list_item.scss
+++ /dev/null
@@ -1,24 +0,0 @@
-.project-list-item {
- &:not(:disabled):not(.disabled) {
- &:focus,
- &:active,
- &:focus:active {
- outline: none;
- box-shadow: none;
- }
- }
-}
-
-// When housed inside a modal, the edge of each item
-// should extend to the edge of the modal.
-.modal-body {
- .project-list-item {
- border-radius: 0;
- margin-left: -$gl-padding;
- margin-right: -$gl-padding;
-
- .project-namespace-name-container {
- overflow: hidden;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework.scss b/app/assets/stylesheets/framework.scss
index 804cc20527..06a8694eb3 100644
--- a/app/assets/stylesheets/framework.scss
+++ b/app/assets/stylesheets/framework.scss
@@ -9,7 +9,6 @@
@import 'framework/animations';
@import 'framework/vue_transitions';
-@import 'framework/banner';
@import 'framework/blocks';
@import 'framework/buttons';
@import 'framework/badges';
diff --git a/app/assets/stylesheets/framework/banner.scss b/app/assets/stylesheets/framework/banner.scss
deleted file mode 100644
index 71bbab2065..0000000000
--- a/app/assets/stylesheets/framework/banner.scss
+++ /dev/null
@@ -1,40 +0,0 @@
-.banner-callout {
- display: flex;
- position: relative;
- align-items: start;
-
- .banner-close {
- position: absolute;
- top: 10px;
- right: 10px;
- opacity: 1;
-
- .dismiss-icon {
- color: $gl-text-color;
- font-size: $gl-font-size;
- }
- }
-
- .banner-graphic {
- margin: 0 $gl-padding $gl-padding 0;
- }
-
- &.banner-non-empty-state {
- border-bottom: 1px solid $border-color;
- }
-
- @include media-breakpoint-down(xs) {
- justify-content: center;
- flex-direction: column;
- align-items: center;
-
- .banner-title,
- .banner-buttons {
- text-align: center;
- }
-
- .banner-graphic {
- margin-left: $gl-padding;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/blocks.scss b/app/assets/stylesheets/framework/blocks.scss
index a0682eabf0..549289450a 100644
--- a/app/assets/stylesheets/framework/blocks.scss
+++ b/app/assets/stylesheets/framework/blocks.scss
@@ -1,9 +1,3 @@
-.centered-light-block {
- text-align: center;
- color: $gl-text-color;
- margin: 20px;
-}
-
.nothing-here-block {
text-align: center;
padding: 16px;
@@ -83,22 +77,6 @@
> p:last-child {
margin-bottom: 0;
}
-
- .block-controls {
- display: flex;
- justify-content: flex-end;
- flex: 1;
-
- .control {
- float: left;
- margin-left: 10px;
- }
- }
-
- &.build-content {
- background-color: $white;
- border-top: 0;
- }
}
.sub-header-block {
@@ -169,31 +147,6 @@
}
}
- &.groups-cover-block {
- background: $white;
- border-bottom: 1px solid $border-color;
- text-align: left;
- padding: 24px 0;
-
- .group-info {
- .cover-title {
- margin-top: 9px;
- }
-
- p {
- margin-bottom: 0;
- }
- }
-
- @include media-breakpoint-down(xs) {
- text-align: center;
-
- .avatar {
- float: none;
- }
- }
- }
-
&.user-cover-block {
padding: 24px 0 0;
@@ -214,19 +167,6 @@
margin-right: auto;
}
}
-
- .group-info {
- h1 {
- display: inline;
- font-weight: $gl-font-weight-normal;
- font-size: 24px;
- color: $gl-text-color;
- }
- }
-}
-
-.block-connector {
- margin-top: -1px;
}
.content-block {
@@ -322,7 +262,7 @@
display: inline-block;
}
- .btn {
+ .btn:not(.split-content-button):not(.dropdown-toggle-split) {
margin: $gl-padding-8 $gl-padding-4;
@include media-breakpoint-down(xs) {
@@ -332,10 +272,6 @@
}
}
-.flex-right {
- margin-left: auto;
-}
-
.code-block {
white-space: pre;
overflow-x: auto;
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ceccec8c5c..e458dfd531 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -247,13 +247,6 @@
}
}
-.btn-terminal {
- svg {
- height: 14px;
- width: $default-icon-size;
- }
-}
-
.btn-lg {
padding: 12px 20px;
}
@@ -281,12 +274,6 @@
}
}
-.btn-align-content {
- display: flex;
- justify-content: center;
- align-items: center;
-}
-
.btn-group {
&.btn-grouped {
@include btn-with-margin;
@@ -347,16 +334,6 @@
}
}
-.btn-static {
- background-color: $gray-light !important;
- border: 1px solid $border-gray-normal;
- cursor: default;
-
- &:active {
- box-shadow: inset 0 0 0 $white;
- }
-}
-
.btn-inverted {
&-secondary {
@include btn-outline($white, $blue-500, $blue-500, $blue-100, $blue-700, $blue-500, $blue-200, $blue-600, $blue-800);
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index a7ce19ffc6..354d273789 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -175,10 +175,6 @@ p.time {
text-shadow: none;
}
-.thin-area {
- height: 150px;
-}
-
// Fix issue with notes & lists creating a bunch of bottom borders.
li.note {
img { max-width: 100%; }
@@ -298,10 +294,6 @@ img.emoji {
margin: 0;
}
-.space-right {
- margin-right: 10px;
-}
-
.alert {
margin-bottom: $gl-padding;
}
@@ -363,14 +355,6 @@ img.emoji {
}
}
-.outline-0 {
- outline: 0;
-
- &:focus {
- outline: 0;
- }
-}
-
/** COMMON CLASSES **/
/**
🚨 Do not use these classes — they are deprecated and being removed. 🚨
diff --git a/app/assets/stylesheets/framework/contextual_sidebar.scss b/app/assets/stylesheets/framework/contextual_sidebar.scss
index f5002a342b..fa1892903a 100644
--- a/app/assets/stylesheets/framework/contextual_sidebar.scss
+++ b/app/assets/stylesheets/framework/contextual_sidebar.scss
@@ -140,7 +140,7 @@
@include gl-border-none;
.avatar.s32 {
- @extend .rect-avatar.s32;
+ border-radius: $border-radius-default;
box-shadow: $avatar-box-shadow;
}
}
diff --git a/app/assets/stylesheets/framework/diffs.scss b/app/assets/stylesheets/framework/diffs.scss
index 568182ad79..23dc16b7e7 100644
--- a/app/assets/stylesheets/framework/diffs.scss
+++ b/app/assets/stylesheets/framework/diffs.scss
@@ -1007,6 +1007,27 @@ table.code {
}
}
+// Notes tweaks for the Changes tab ONLY
+.diff-tr {
+ .timeline-discussion-body {
+ clear: left;
+
+ .note-body {
+ margin-top: 0 !important;
+ }
+ }
+
+ .timeline-entry img.avatar {
+ margin-top: -2px;
+ margin-right: $gl-padding-8;
+ }
+
+ // tiny adjustment to vertical align with the note header text
+ .discussion-collapsible .timeline-icon {
+ padding-top: 2px;
+ }
+}
+
.files:not([data-can-create-note]) .frame {
cursor: auto;
}
@@ -1125,7 +1146,7 @@ table.code {
}
.discussion-collapsible {
- margin: 0 $gl-padding $gl-padding 71px;
+ margin: 0 $gl-padding $gl-padding;
.notes {
border-radius: $border-radius-default;
diff --git a/app/assets/stylesheets/framework/dropdowns.scss b/app/assets/stylesheets/framework/dropdowns.scss
index b05fbfaae6..7f960e3da5 100644
--- a/app/assets/stylesheets/framework/dropdowns.scss
+++ b/app/assets/stylesheets/framework/dropdowns.scss
@@ -76,6 +76,7 @@
}
.dropdown-toggle,
+.dropdown-menu-toggle,
.confidential-merge-request-fork-group .dropdown-toggle {
padding: 6px 8px 6px 10px;
background-color: $white;
@@ -131,7 +132,6 @@
// This is double classed to solve a specificity issue with the gitlab ui buttons
.dropdown-menu-toggle.dropdown-menu-toggle {
- @extend .dropdown-toggle;
justify-content: flex-start;
overflow: hidden;
padding-right: 25px;
@@ -425,21 +425,10 @@
}
}
-.dropdown-menu-drop-up {
- top: auto;
- bottom: 100%;
-}
-
.dropdown-menu-large {
width: 340px;
}
-.dropdown-menu-no-wrap {
- a {
- white-space: normal;
- }
-}
-
.dropdown-menu-full-width {
width: 100%;
}
@@ -662,13 +651,6 @@
padding-right: 10px;
}
-.dropdown-due-date-footer {
- padding-top: 0;
- margin-left: 10px;
- margin-right: 10px;
- border-top: 0;
-}
-
.dropdown-footer-list {
font-size: 14px;
@@ -742,24 +724,6 @@
}
}
-.dropdown-menu-due-date {
- .dropdown-content {
- max-height: 230px;
- }
-
- .pika-single {
- position: relative !important;
- top: 0 !important;
- border: 0;
- box-shadow: none;
- }
-
- .pika-lendar {
- margin-top: -5px;
- margin-bottom: 0;
- }
-}
-
.dropdown-menu-inner-title {
display: block;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index c366bf8009..2a46e50f0d 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -196,14 +196,6 @@ label {
}
}
-@include media-breakpoint-down(xs) {
- .remember-me {
- .remember-me-checkbox {
- margin-top: 0;
- }
- }
-}
-
.input-icon-wrapper,
.select-wrapper {
position: relative;
diff --git a/app/assets/stylesheets/framework/job_log.scss b/app/assets/stylesheets/framework/job_log.scss
index d48a511667..f77f64f1d7 100644
--- a/app/assets/stylesheets/framework/job_log.scss
+++ b/app/assets/stylesheets/framework/job_log.scss
@@ -5,10 +5,10 @@
font-size: 13px;
word-break: break-all;
word-wrap: break-word;
- color: color-yiq($builds-trace-bg);
+ color: color-yiq($builds-log-bg);
border-radius: $border-radius-small;
min-height: 42px;
- background-color: $builds-trace-bg;
+ background-color: $builds-log-bg;
}
.log-line {
@@ -42,10 +42,6 @@
}
}
-.log-duration-badge {
- background: $gray-300;
-}
-
.loader-animation {
@include build-loader-animation;
}
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 7315bce1ed..ef29463564 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -153,6 +153,10 @@
vertical-align: middle;
margin-bottom: 3px;
}
+
+ .dropdown-chevron {
+ margin-bottom: 0;
+ }
}
@include media-breakpoint-down(xs) {
diff --git a/app/assets/stylesheets/framework/media_object.scss b/app/assets/stylesheets/framework/media_object.scss
index 89c561479c..b573052c14 100644
--- a/app/assets/stylesheets/framework/media_object.scss
+++ b/app/assets/stylesheets/framework/media_object.scss
@@ -6,7 +6,3 @@
.media-body {
flex: 1;
}
-
-.media-body-wrap {
- flex-grow: 1;
-}
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index fcf86680bb..33f7aa4dba 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -239,7 +239,7 @@
/*
* Mixin that handles the container for the job logs (CI/CD and kubernetes pod logs)
*/
-@mixin build-trace($background: $black) {
+@mixin build-log($background: $black) {
background: $background;
color: $gray-darkest;
white-space: pre;
@@ -253,13 +253,13 @@
display: block;
}
- &.build-trace-rounded {
+ &.build-log-rounded {
border-radius: $gl-border-radius-base;
}
}
// Used in EE for Web Terminal
-@mixin build-trace-bar($height) {
+@mixin build-log-bar($height) {
height: $height;
min-height: $height;
background: var(--gray-50, $gray-50);
@@ -268,8 +268,8 @@
padding: $grid-size;
}
-@mixin build-trace-top-bar($height) {
- @include build-trace-bar($height);
+@mixin build-log-top-bar($height) {
+ @include build-log-bar($height);
position: -webkit-sticky;
position: sticky;
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 48a18e0d14..c9b17f5d5c 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -1,13 +1,3 @@
-.modal-xl {
- max-width: 98%;
-}
-
-.modal-1040 {
- @include media-breakpoint-up(xl) {
- max-width: 1040px;
- }
-}
-
.modal-header {
background-color: $modal-body-bg;
@@ -111,30 +101,3 @@ body.modal-open {
}
}
}
-
-.recaptcha-modal .recaptcha-form {
- display: inline-block;
-
- .recaptcha {
- margin: 0;
- }
-}
-
-.issues-import-modal,
-.issuable-export-modal {
- .modal-body {
- padding: 0;
-
- .modal-subheader {
- justify-content: flex-start;
- align-items: center;
- border-bottom: 1px solid $modal-border-color;
- padding: 14px;
- }
-
- .modal-text {
- padding: $gl-padding-24 $gl-padding;
- min-height: $modal-body-height;
- }
- }
-}
diff --git a/app/assets/stylesheets/framework/panels.scss b/app/assets/stylesheets/framework/panels.scss
index d9c93fed1c..e5b2b85336 100644
--- a/app/assets/stylesheets/framework/panels.scss
+++ b/app/assets/stylesheets/framework/panels.scss
@@ -39,10 +39,6 @@
}
}
-.card-empty-heading {
- border-bottom: 0;
-}
-
.card-body {
padding: $gl-padding;
diff --git a/app/assets/stylesheets/framework/secondary_navigation_elements.scss b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
index 06eebb9543..685f1f413e 100644
--- a/app/assets/stylesheets/framework/secondary_navigation_elements.scss
+++ b/app/assets/stylesheets/framework/secondary_navigation_elements.scss
@@ -1,6 +1,6 @@
// For tabbed navigation links, scrolling tabs, etc. For all top/main navigation,
// please check nav.scss
-.nav-links:not(.quick-links) {
+.nav-links {
display: flex;
padding: 0;
margin: 0;
diff --git a/app/assets/stylesheets/framework/selects.scss b/app/assets/stylesheets/framework/selects.scss
index d8ce6826fc..900cf9fa4d 100644
--- a/app/assets/stylesheets/framework/selects.scss
+++ b/app/assets/stylesheets/framework/selects.scss
@@ -50,18 +50,6 @@
}
}
-.namespace-result {
- .namespace-kind {
- color: $gray-300;
- font-weight: $gl-font-weight-normal;
- }
-
- .namespace-path {
- margin-left: 10px;
- font-weight: $gl-font-weight-bold;
- }
-}
-
.ajax-users-dropdown {
min-width: 250px !important;
}
diff --git a/app/assets/stylesheets/framework/sidebar.scss b/app/assets/stylesheets/framework/sidebar.scss
index 6b3201ba2b..6c7fc25f2d 100644
--- a/app/assets/stylesheets/framework/sidebar.scss
+++ b/app/assets/stylesheets/framework/sidebar.scss
@@ -16,21 +16,6 @@
transition: padding $sidebar-transition-duration;
}
-.nav-header-btn {
- padding: 10px $gl-sidebar-padding;
- color: inherit;
- transition-duration: 0.3s;
- position: absolute;
- top: 0;
- cursor: pointer;
-
- &:hover,
- &:focus {
- color: $white;
- text-decoration: none;
- }
-}
-
.right-sidebar-collapsed {
padding-right: 0;
diff --git a/app/assets/stylesheets/framework/snippets.scss b/app/assets/stylesheets/framework/snippets.scss
index 4718480435..c59e70c80d 100644
--- a/app/assets/stylesheets/framework/snippets.scss
+++ b/app/assets/stylesheets/framework/snippets.scss
@@ -4,11 +4,6 @@
font-weight: $gl-font-weight-bold;
}
- .snippet-filename {
- color: $gl-text-color-secondary;
- font-weight: normal;
- }
-
.snippet-info {
color: $gl-text-color-secondary;
}
diff --git a/app/assets/stylesheets/framework/sortable.scss b/app/assets/stylesheets/framework/sortable.scss
index 25868061d0..953c42219a 100644
--- a/app/assets/stylesheets/framework/sortable.scss
+++ b/app/assets/stylesheets/framework/sortable.scss
@@ -36,61 +36,6 @@
}
}
-.related-issues-list-item {
- .card-body,
- .issuable-info-container {
- padding: $gl-padding-4 $gl-padding-4 $gl-padding-4 $gl-padding;
-
- .block-truncated {
- padding: $gl-padding-8 0;
- line-height: $gl-btn-line-height;
- }
-
- @include media-breakpoint-down(md) {
- padding-left: $gl-padding;
-
- .block-truncated {
- flex-direction: column-reverse;
- padding: $gl-padding-4 0;
-
- .text-secondary {
- margin-top: $gl-padding-4;
- }
-
- .issue-token-title-text {
- display: block;
- }
- }
-
- .issue-item-remove-button {
- align-self: baseline;
- }
- }
-
- @include media-breakpoint-only(md) {
- .block-truncated .issue-token-title-text {
- white-space: nowrap;
- }
-
- .issue-item-remove-button {
- align-self: center;
- }
- }
-
- @include media-breakpoint-down(sm) {
- padding-left: $gl-padding-8;
-
- .block-truncated .issue-token-title-text {
- white-space: normal;
- }
- }
- }
-
- &.is-dragging {
- padding: 0;
- }
-}
-
.is-dragging {
// Important because plugin sets inline CSS
opacity: 1 !important;
diff --git a/app/assets/stylesheets/framework/tables.scss b/app/assets/stylesheets/framework/tables.scss
index 92405c00c5..c6bc8fa0ea 100644
--- a/app/assets/stylesheets/framework/tables.scss
+++ b/app/assets/stylesheets/framework/tables.scss
@@ -23,6 +23,7 @@ table {
@include gl-text-gray-500;
}
+ .md &:not(.code),
&.table {
margin-bottom: $gl-padding;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index aeb3bb2286..cb36c4e576 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -75,6 +75,15 @@
details {
margin-bottom: $gl-padding;
+
+ > *:not(summary) {
+ margin-left: $gl-spacing-scale-5;
+ }
+ }
+
+ summary > * {
+ display: inline-block;
+ margin-bottom: 0;
}
// Single code lines should wrap
@@ -160,8 +169,6 @@
}
table:not(.code) {
- @extend .table;
- @extend .table-bordered;
margin: 16px 0;
color: $gl-text-color;
border: 0;
@@ -172,9 +179,11 @@
tbody {
background-color: $white;
- td {
- border-color: $gray-100;
- }
+ }
+
+ td,
+ th {
+ border: 1px solid $border-color;
}
tr {
@@ -478,6 +487,7 @@
font-size: larger;
}
+ figcaption,
.small {
font-size: smaller;
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 099dfa28b9..026aeeb1e8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -697,7 +697,7 @@ $blame-blue: #254e77;
/*
* Builds
*/
-$builds-trace-bg: #111;
+$builds-log-bg: #111;
$job-log-highlight-height: 18px;
$job-log-line-padding: 55px;
$job-line-number-width: 50px;
@@ -759,7 +759,6 @@ $help-shortcut-header-color: #333;
*/
$issues-today-bg: #f3fff2 !default;
$issues-today-border: #e1e8d5 !default;
-$compare-display-color: #888;
/*
* Label
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index 62abf4a768..10df532e33 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -40,15 +40,6 @@
border: 0;
}
-.zen-control-full {
- color: $gl-text-color-secondary;
-
- &:hover {
- color: $blue-600;
- text-decoration: none;
- }
-}
-
.zen-control-leave {
display: none;
color: $gl-text-color;
diff --git a/app/assets/stylesheets/page_bundles/_ide_mixins.scss b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
index 48b8a7230b..bbc47c5cd5 100644
--- a/app/assets/stylesheets/page_bundles/_ide_mixins.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_mixins.scss
@@ -4,7 +4,7 @@
height: 100%;
.top-bar {
- @include build-trace-bar(35px);
+ @include build-log-bar(35px);
top: 0;
font-size: 12px;
diff --git a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
index 7336d555f7..25a565ce2b 100644
--- a/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
+++ b/app/assets/stylesheets/page_bundles/_ide_theme_overrides.scss
@@ -44,7 +44,7 @@
background-color: var(--ide-background, $badge-bg);
}
- .nav-links:not(.quick-links) li:not(.md-header-toolbar) a,
+ .nav-links li:not(.md-header-toolbar) a,
.gl-tabs-nav li a,
.dropdown-menu-inner-content,
.file-row .file-row-icon svg,
@@ -52,7 +52,7 @@
color: var(--ide-text-color-secondary, $gl-text-color-secondary);
}
- .nav-links:not(.quick-links) li:not(.md-header-toolbar),
+ .nav-links li:not(.md-header-toolbar),
.gl-tabs-nav li {
&:hover a,
&.active a,
@@ -148,7 +148,7 @@
.md blockquote,
.md table:not(.code) tbody td,
.md table:not(.code) tr th,
- .nav-links:not(.quick-links),
+ .nav-links,
.gl-tabs-nav,
.common-note-form .md-area.is-focused .nav-links {
border-color: var(--ide-border-color-alt, $white-dark);
@@ -259,6 +259,7 @@
.dropdown-menu-toggle {
border-color: var(--ide-btn-default-border, $gray-darkest);
+ background-color: var(--ide-input-background, transparent);
&:hover,
&:focus {
@@ -310,7 +311,7 @@
border-color: var(--ide-background, $border-color);
background-color: var(--ide-dropdown-background, $white);
- .nav-links:not(.quick-links) {
+ .nav-links {
background-color: var(--ide-dropdown-hover-background, $white);
border-color: var(--ide-dropdown-hover-background, $border-color);
}
diff --git a/app/assets/stylesheets/page_bundles/boards.scss b/app/assets/stylesheets/page_bundles/boards.scss
index 4806f4b054..a3ec2167b1 100644
--- a/app/assets/stylesheets/page_bundles/boards.scss
+++ b/app/assets/stylesheets/page_bundles/boards.scss
@@ -430,7 +430,7 @@
height: $input-height;
}
-.issue-boards-content {
+.issue-boards-content:not(.breadcrumbs) {
isolation: isolate;
}
diff --git a/app/assets/stylesheets/page_bundles/build.scss b/app/assets/stylesheets/page_bundles/build.scss
index ec41909bee..ed62e05542 100644
--- a/app/assets/stylesheets/page_bundles/build.scss
+++ b/app/assets/stylesheets/page_bundles/build.scss
@@ -1,8 +1,8 @@
@import 'mixins_and_variables_and_functions';
.build-page {
- .build-trace {
- @include build-trace();
+ .build-log {
+ @include build-log();
}
.archived-job {
@@ -18,7 +18,7 @@
}
.top-bar {
- @include build-trace-top-bar(50px);
+ @include build-log-top-bar(50px);
&.has-archived-block {
top: $header-height + 28px;
diff --git a/app/assets/stylesheets/page_bundles/merge_requests.scss b/app/assets/stylesheets/page_bundles/merge_requests.scss
index 28354b8385..7d1230b022 100644
--- a/app/assets/stylesheets/page_bundles/merge_requests.scss
+++ b/app/assets/stylesheets/page_bundles/merge_requests.scss
@@ -109,3 +109,12 @@
}
}
}
+
+// TODO: Move to GitLab UI
+.mr-extenson-scrim {
+ background: linear-gradient(to bottom, rgba($gray-light, 0), rgba($gray-light, 1));
+
+ .gl-dark & {
+ background: linear-gradient(to bottom, rgba(#333, 0), rgba(#333, 1));
+ }
+}
diff --git a/app/assets/stylesheets/page_bundles/pipeline.scss b/app/assets/stylesheets/page_bundles/pipeline.scss
index 206c2eb09d..c8b1b6cf9a 100644
--- a/app/assets/stylesheets/page_bundles/pipeline.scss
+++ b/app/assets/stylesheets/page_bundles/pipeline.scss
@@ -44,14 +44,14 @@
line-height: initial;
}
- .build-trace-row td {
+ .build-log-row td {
border-top: 0;
border-bottom-width: 1px;
border-bottom-style: solid;
padding-top: 0;
}
- .build-trace {
+ .build-log {
width: 100%;
text-align: left;
margin-top: $gl-padding;
@@ -93,7 +93,7 @@
}
.build-state,
- .build-trace-row {
+ .build-log-row {
> td:last-child {
padding-right: 0;
}
@@ -108,12 +108,12 @@
margin-top: 2 * $gl-padding;
}
- .build-trace-container {
+ .build-log-container {
padding-top: $gl-padding;
padding-bottom: $gl-padding;
}
- .build-trace {
+ .build-log {
margin-bottom: 0;
margin-top: 0;
}
@@ -221,8 +221,8 @@
}
.test-reports-table {
- .build-trace {
- @include build-trace();
+ .build-log {
+ @include build-log();
}
}
diff --git a/app/assets/stylesheets/page_bundles/signup.scss b/app/assets/stylesheets/page_bundles/signup.scss
index 57e5d2411d..4fc671dace 100644
--- a/app/assets/stylesheets/page_bundles/signup.scss
+++ b/app/assets/stylesheets/page_bundles/signup.scss
@@ -26,14 +26,6 @@
}
}
- .omniauth-btn {
- width: 48%;
-
- @include media-breakpoint-down(md) {
- width: 100%;
- }
- }
-
.decline-page {
width: 350px;
}
diff --git a/app/assets/stylesheets/page_bundles/wiki.scss b/app/assets/stylesheets/page_bundles/wiki.scss
index 1e6567189b..c64e159c64 100644
--- a/app/assets/stylesheets/page_bundles/wiki.scss
+++ b/app/assets/stylesheets/page_bundles/wiki.scss
@@ -152,5 +152,5 @@ ul.wiki-pages-list.content-list {
}
.wiki-form .markdown-area {
- max-height: none;
+ max-height: 55vh;
}
diff --git a/app/assets/stylesheets/pages/ci_projects.scss b/app/assets/stylesheets/pages/ci_projects.scss
deleted file mode 100644
index fbe1f3081a..0000000000
--- a/app/assets/stylesheets/pages/ci_projects.scss
+++ /dev/null
@@ -1,54 +0,0 @@
-.ci-body {
- .project-title {
- margin: 0;
- color: $common-gray-dark;
- font-size: 20px;
- line-height: 1.5;
- }
-
- .builds,
- .projects-table {
- .light {
- border-color: $border-color;
- }
-
- th,
- td {
- padding: 10px $gl-padding;
- }
-
- td {
- color: $gl-text-color;
- vertical-align: middle !important;
-
- a {
- font-weight: $gl-font-weight-normal;
- text-decoration: none;
- }
- }
- }
-
- .commit-info {
- .attr-name {
- margin-right: 5px;
- }
-
- pre.commit-message {
- background: none;
- padding: 0;
- border: 0;
- margin: 20px 0;
- border-radius: 0;
- }
- }
-
- .loading {
- font-size: 20px;
- }
-
- .ci-charts {
- fieldset {
- margin-bottom: 16px;
- }
- }
-}
diff --git a/app/assets/stylesheets/pages/clusters.scss b/app/assets/stylesheets/pages/clusters.scss
index d233adbf3d..de27ca2e5e 100644
--- a/app/assets/stylesheets/pages/clusters.scss
+++ b/app/assets/stylesheets/pages/clusters.scss
@@ -1,88 +1,4 @@
-.edit-cluster-form {
- .clipboard-addon {
- background-color: $white;
- }
-}
-
-.cluster-application-row {
- background: $gray-lighter;
-
- &.cluster-application-installed {
- background: none;
- }
-
- .settings-message {
- padding: $gl-vert-padding $gl-padding-8;
- }
-}
-
-@media (min-width: map-get($grid-breakpoints, md)) {
- .cluster-application-list {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
- }
-
- .cluster-application-row {
- border-bottom: 1px solid $border-color;
- padding: $gl-padding;
-
- &:last-child {
- border-bottom: 0;
- border-bottom-left-radius: calc(#{$border-radius-default} - 1px);
- border-bottom-right-radius: calc(#{$border-radius-default} - 1px);
- }
- }
-}
-
-.cluster-application-logo {
- border: 3px solid $white;
- box-shadow: 0 0 0 1px $gray-normal;
-
- &.avatar:hover {
- border-color: $white;
- }
-}
-
-.cluster-application-warning {
- font-weight: bold;
- text-align: center;
- padding: $gl-padding;
- border-bottom: 1px solid $white-normal;
-
- .svg-container {
- display: inline-block;
- vertical-align: middle;
- margin-right: $gl-padding-8;
- width: 40px;
- height: 40px;
- }
-}
-
-.cluster-application-description {
- flex: 1;
-}
-
-.cluster-application-disabled {
- opacity: 0.5;
-}
-
-.clusters-dropdown-menu {
- max-width: 100%;
-}
-
-.clusters-error-alert {
- width: 100%;
-}
-
.clusters-container {
- .nav-bar-right {
- padding: $gl-padding-top $gl-padding;
- }
-
- .card {
- margin-bottom: $gl-vert-padding;
- }
-
.empty-state .svg-content img {
width: 145px;
}
@@ -100,71 +16,4 @@
@include gl-flex-wrap;
}
}
-
- .top-area .nav-controls > .btn.btn-add-cluster {
- margin-right: 0;
- }
-
- .clusters-table {
- background-color: $gray-light;
- padding: $gl-padding-8;
- }
-
- .badge-light {
- background-color: $white-normal;
- }
-
- .gl-responsive-table-row {
- padding: $gl-padding;
- border: 0;
-
- &.table-row-header {
- // stylelint-disable-next-line
- background-color: none;
- border: 0;
- font-weight: bold;
- color: $gray-500;
- }
- }
-}
-
-.cluster-warning {
- @include alert-variant(theme-color-level('warning', $alert-bg-level), theme-color-level('warning', $alert-border-level), theme-color-level('warning', $alert-color-level));
-}
-
-.gcp-signup-offer {
- border-left-color: $blue-500;
-
- svg {
- fill: $blue-500;
- vertical-align: middle;
- }
-
- .gcp-signup-offer--content {
- display: flex;
-
- h4 {
- font-size: 16px;
- line-height: 24px;
- }
-
- .gcp-signup-offer--icon {
- align-self: flex-start;
- }
- }
-}
-
-.cluster-deployments-warning {
- color: $orange-500;
-}
-
-.badge.pods-badge {
- color: $black;
- font-weight: $gl-font-weight-bold;
-}
-
-.cluster-status-indicator {
- &.disabled {
- background-color: $gray-400;
- }
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index bc4dbf695c..7f35b8fab4 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -82,11 +82,6 @@
}
}
-.commits-compare-switch {
- float: left;
- margin-right: 9px;
-}
-
.commit-header {
padding: 5px 10px;
background-color: $gray-light;
diff --git a/app/assets/stylesheets/pages/environment_logs.scss b/app/assets/stylesheets/pages/environment_logs.scss
index 03993e5321..f8f4007614 100644
--- a/app/assets/stylesheets/pages/environment_logs.scss
+++ b/app/assets/stylesheets/pages/environment_logs.scss
@@ -40,8 +40,8 @@
height: 100%;
}
- .build-trace {
- @include build-trace($black);
+ .build-log {
+ @include build-log($black);
}
.gl-infinite-scroll-legend {
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 94912b1c64..c597d2dd8d 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -220,21 +220,12 @@
}
.cross-project-reference {
- color: inherit;
-
span {
- white-space: nowrap;
width: 85%;
- overflow: hidden;
- position: relative;
- display: inline-block;
- text-overflow: ellipsis;
}
button {
- float: right;
padding: 1px 5px;
- background-color: $gray-light;
}
}
@@ -563,35 +554,17 @@
}
}
-.participants-list {
- display: flex;
- flex-wrap: wrap;
-}
-
-.user-list {
- display: flex;
- flex-wrap: wrap;
-}
-
.participants-author {
- display: inline-block;
- padding: 0 $gl-padding-8 $gl-padding-8 0;
-
&:nth-of-type(7n) {
padding-right: 0;
}
- .author-link {
- display: block;
- }
-
.avatar.avatar-inline {
margin: 0;
}
}
.user-item {
- display: inline-block;
padding: 5px;
flex-basis: 20%;
@@ -673,40 +646,40 @@
.issuable-info-container {
flex: 1;
display: flex;
+ }
- .issuable-main-info {
- flex: 1 auto;
- margin-right: 10px;
- min-width: 0;
+ .issuable-main-info {
+ flex: 1 auto;
+ margin-right: 10px;
+ min-width: 0;
- .issue-weight-icon,
- .issue-estimate-icon {
- vertical-align: sub;
- }
+ .issue-weight-icon,
+ .issue-estimate-icon {
+ vertical-align: sub;
+ }
+ }
+
+ .issuable-meta {
+ display: flex;
+ flex-direction: column;
+ align-items: flex-end;
+ flex: 1 0 auto;
+
+ .controls {
+ margin-bottom: 2px;
+ line-height: 20px;
+ padding: 0;
}
+ .issue-updated-at {
+ line-height: 20px;
+ }
+ }
+
+ @include media-breakpoint-down(xs) {
.issuable-meta {
- display: flex;
- flex-direction: column;
- align-items: flex-end;
- flex: 1 0 auto;
-
- .controls {
- margin-bottom: 2px;
- line-height: 20px;
- padding: 0;
- }
-
- .issue-updated-at {
- line-height: 20px;
- }
- }
-
- @include media-breakpoint-down(xs) {
- .issuable-meta {
- .controls li {
- margin-right: 0;
- }
+ .controls li {
+ margin-right: 0;
}
}
}
@@ -773,38 +746,15 @@
}
}
-.add-issuable-form-input-token-list {
- display: flex;
- flex-wrap: wrap;
- align-items: baseline;
- list-style: none;
- margin-bottom: 0;
- padding-left: 0;
-}
-
-.add-issuable-form-token-list-item {
- max-width: 100%;
- margin-bottom: $gl-vert-padding;
- margin-right: 5px;
-}
-
-.add-issuable-form-input-list-item {
- flex: 1;
- min-width: 200px;
- margin-bottom: $gl-vert-padding;
-}
-
-.add-issuable-form-input {
- width: 100%;
- border: 0;
-
- &:focus {
- outline: none;
+.add-issuable-form-input-wrapper {
+ &.focus {
+ border-color: $blue-300;
+ box-shadow: 0 0 4px $dropdown-input-focus-shadow;
}
-}
-.add-issuable-form-actions {
- margin-top: $gl-padding;
+ .gl-show-field-errors &.form-control:not(textarea) {
+ height: auto;
+ }
}
.time-tracker {
@@ -839,18 +789,7 @@
}
.compare-display-container {
- display: flex;
- justify-content: space-between;
- margin-top: 5px;
-
- .compare-display {
- font-size: 13px;
- color: $compare-display-color;
-
- .compare-value {
- color: $gl-text-color;
- }
- }
+ font-size: 13px;
}
.time-tracking-help-state {
@@ -938,22 +877,6 @@
vertical-align: sub;
}
-.suggestion-confidential {
- color: $orange-500;
-}
-
-.suggestion-state-open {
- color: $green-500;
-}
-
-.suggestion-state-closed {
- color: $blue-500;
-}
-
-.suggestion-help-hover {
- cursor: help;
-}
-
.suggestion-footer {
font-size: 12px;
line-height: 15px;
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 461d6a29b3..880231f564 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -1,3 +1,51 @@
+.issue-token-link {
+ &[href] {
+ color: $blue-600;
+ }
+
+ &:hover,
+ &:focus {
+ outline: none;
+ text-decoration: none;
+ }
+}
+
+.issue-token-reference {
+ margin-right: 1px;
+ background-color: $gray-lighter;
+ transition: background $general-hover-transition-duration $general-hover-transition-curve, color $general-hover-transition-duration $general-hover-transition-curve;
+
+ .issue-token:hover &,
+ .issue-token-link:focus > & {
+ background-color: $gray-normal;
+ color: $blue-800;
+ text-decoration: none;
+ }
+}
+
+.issue-token-title {
+ background-color: $gray-normal;
+ transition: background $general-hover-transition-duration $general-hover-transition-curve;
+
+ .issue-token:hover &,
+ .issue-token-link:focus > & {
+ background-color: $border-gray-normal;
+ }
+}
+
+.issue-token-remove-button {
+ background-color: $gray-normal;
+ transition: background $general-hover-transition-duration $general-hover-transition-curve;
+
+ &:hover,
+ &:focus,
+ .issue-token:hover &,
+ .issue-token-link:focus + & {
+ background-color: $border-gray-normal;
+ outline: none;
+ }
+}
+
.issue-realtime-pre-pulse {
opacity: 0;
}
@@ -7,36 +55,8 @@
opacity: 1;
}
-.check-all-holder {
- line-height: 36px;
- float: left;
- margin-right: 15px;
-}
-
-form.edit-issue {
- margin: 0;
-}
-
-ul.related-merge-requests > li {
- display: flex;
- align-items: center;
-
- .merge-request-id {
- flex-shrink: 0;
- }
-
- .merge-request-info {
- margin-left: 5px;
- }
-
- gl-emoji {
- font-size: 1em;
- }
-}
-
-.related-branches-title {
- font-size: 16px;
- font-weight: $gl-font-weight-bold;
+ul.related-merge-requests > li gl-emoji {
+ font-size: 1em;
}
.merge-request-status {
@@ -92,35 +112,12 @@ ul.related-merge-requests > li {
}
}
-.issues-footer {
- padding-top: $gl-padding;
- padding-bottom: 37px;
-}
-
-.issues-nav-controls,
-.new-branch-col {
- font-size: 0;
-}
-
.issues-nav-controls {
.btn-group:empty {
display: none;
}
}
-.email-modal-input-group {
- margin-bottom: 10px;
-
- .form-control {
- background-color: $white;
- }
-
- .btn {
- background-color: $gray-light;
- border: 1px solid $border-gray-normal;
- }
-}
-
.recaptcha {
margin-bottom: 30px;
}
diff --git a/app/assets/stylesheets/pages/issues/issue_count_badge.scss b/app/assets/stylesheets/pages/issues/issue_count_badge.scss
deleted file mode 100644
index f2283e02ad..0000000000
--- a/app/assets/stylesheets/pages/issues/issue_count_badge.scss
+++ /dev/null
@@ -1,10 +0,0 @@
-.issue-count-badge,
-.mr-count-badge {
- padding: 5px $gl-padding-8;
-}
-
-.issue-count-badge-count,
-.mr-count-badge-count {
- display: inline-flex;
- align-items: center;
-}
diff --git a/app/assets/stylesheets/pages/login.scss b/app/assets/stylesheets/pages/login.scss
index 773935f4c7..71ddbf175e 100644
--- a/app/assets/stylesheets/pages/login.scss
+++ b/app/assets/stylesheets/pages/login.scss
@@ -99,11 +99,6 @@
padding: 0;
border: 0;
background: none;
- margin-bottom: $gl-padding;
- }
-
- .omniauth-btn {
- width: 100%;
}
}
@@ -206,6 +201,12 @@
padding: 0;
height: 100%;
+ &.with-system-header {
+ .login-page-broadcast {
+ margin-top: $system-header-height + $header-height;
+ }
+ }
+
// Fixes footer container to bottom of viewport
body {
// offset height of fixed header + 1 to avoid scroll
diff --git a/app/assets/stylesheets/pages/merge_requests.scss b/app/assets/stylesheets/pages/merge_requests.scss
index 071a5be073..cec8d8a29c 100644
--- a/app/assets/stylesheets/pages/merge_requests.scss
+++ b/app/assets/stylesheets/pages/merge_requests.scss
@@ -474,31 +474,6 @@ $tabs-holder-z-index: 250;
}
}
}
-
- .merge-request-labels {
- display: inline-block;
- }
-}
-
-.merge-request-angle {
- text-align: center;
- margin: 0 auto;
- font-size: 2em;
- line-height: 1.1;
-}
-
-// hide mr close link for inline diff comment form
-.diff-file .close-mr-link,
-.diff-file .reopen-mr-link {
- display: none;
-}
-
-.mr-links {
- padding-left: $gl-padding-8 + $status-icon-size + $gl-btn-padding;
-
- &:last-child {
- padding-bottom: $gl-padding;
- }
}
.mr-info-list {
@@ -649,10 +624,6 @@ $tabs-holder-z-index: 250;
}
}
-.target-branch-select-dropdown-container {
- position: relative;
-}
-
.assign-to-me-link {
padding-left: 12px;
white-space: nowrap;
@@ -667,12 +638,6 @@ $tabs-holder-z-index: 250;
}
}
-.merged-buttons {
- .btn {
- float: left;
- }
-}
-
.mr-version-controls {
position: relative;
z-index: $tabs-holder-z-index + 10;
@@ -1040,3 +1005,17 @@ $tabs-holder-z-index: 250;
margin-bottom: 1px;
}
}
+
+.mr-widget-extension-icon::before {
+ @include gl-content-empty;
+ @include gl-absolute;
+ @include gl-left-0;
+ @include gl-top-0;
+ @include gl-opacity-3;
+ @include gl-border-solid;
+ @include gl-border-4;
+ @include gl-rounded-full;
+
+ width: 24px;
+ height: 24px;
+}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 3c0f10eb5c..1c408f6d98 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -24,12 +24,6 @@
margin: $gl-padding 0 0;
}
- .note-preview-holder {
- > p {
- overflow-x: auto;
- }
- }
-
img {
max-width: 100%;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 17bc40b4de..04da75b586 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -125,18 +125,17 @@ $system-note-svg-size: 16px;
}
}
+ .timeline-discussion-body {
+ margin-top: -$gl-padding-8;
+ }
+
.discussion {
display: block;
position: relative;
.timeline-discussion-body {
- margin-top: -$gl-padding-8;
overflow-x: auto;
overflow-y: hidden;
-
- .note-body {
- margin-top: $gl-padding-8;
- }
}
.diff-content {
@@ -586,17 +585,47 @@ $system-note-svg-size: 16px;
.note-header {
display: flex;
justify-content: space-between;
+ flex-wrap: wrap;
+
+ > .note-header-info,
+ > .note-actions {
+ flex-grow: 1;
+ flex-shrink: 1;
+ }
+}
+
+.note {
+ @include notes-media('max', map-get($grid-breakpoints, sm) - 1) {
+ .note-header {
+ .note-actions {
+ flex-wrap: wrap;
+ margin-bottom: $gl-padding-12;
+
+ > :first-child {
+ margin-left: 0;
+ }
+ }
+ }
+
+ .note-header-author-name {
+ display: block;
+ }
+ }
}
.note-header-info {
min-width: 0;
- padding-bottom: $gl-padding-8;
&.discussion {
padding-bottom: 0;
}
}
+.note-header-info,
+.note-actions {
+ padding-bottom: $gl-padding-8;
+}
+
.system-note .note-header-info {
padding-bottom: 0;
}
@@ -667,7 +696,8 @@ $system-note-svg-size: 16px;
.note-actions {
align-self: flex-start;
- flex-shrink: 0;
+ justify-content: flex-end;
+ flex-shrink: 1;
display: inline-flex;
align-items: center;
margin-left: 10px;
diff --git a/app/assets/stylesheets/pages/notifications.scss b/app/assets/stylesheets/pages/notifications.scss
index 298de33888..2fd2757cf0 100644
--- a/app/assets/stylesheets/pages/notifications.scss
+++ b/app/assets/stylesheets/pages/notifications.scss
@@ -4,11 +4,6 @@
width: 100%;
}
- .notification-form {
- display: block;
- }
-
- .notifications-btn,
.btn-group {
width: 100%;
}
diff --git a/app/assets/stylesheets/pages/pages.scss b/app/assets/stylesheets/pages/pages.scss
index 93caa345f8..ebaf875ad8 100644
--- a/app/assets/stylesheets/pages/pages.scss
+++ b/app/assets/stylesheets/pages/pages.scss
@@ -55,16 +55,4 @@
border-bottom-right-radius: $border-radius-default;
border-top-right-radius: $border-radius-default;
}
-
- &.floating-status-badge {
- position: absolute;
- right: $gl-padding-24;
- bottom: $gl-padding-4;
- margin-bottom: 0;
- }
-}
-
-.form-control.has-floating-status-badge {
- position: relative;
- padding-right: 120px;
}
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index de9e0c6f70..af9f10c9a2 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -1,9 +1,3 @@
-.profile-avatar-form-option {
- hr {
- margin: 10px 0;
- }
-}
-
.avatar-image {
margin-bottom: $grid-size;
@@ -23,41 +17,6 @@
display: inline-block;
}
-.private-tokens-reset div.reset-action:not(:first-child) {
- padding-top: 15px;
-}
-
-.oauth-buttons {
- .btn-group {
- margin-right: 10px;
- }
-
- .btn {
- line-height: 40px;
- height: 42px;
- padding: 0 12px;
-
- img {
- width: 32px;
- height: 32px;
- }
- }
-}
-
-// Profile > Account > Two Factor Authentication
-.two-factor-new {
- .manual-instructions {
- h3 {
- margin-top: 0;
- }
-
- // Slightly increase the size of the details so they're easier to read
- dl {
- font-size: 1.1em;
- }
- }
-}
-
.account-well {
padding: 10px;
background-color: $gray-light;
@@ -191,10 +150,6 @@
}
}
-.personal-access-tokens-never-expires-label {
- color: $note-disabled-comment-color;
-}
-
.created-personal-access-token-container {
.btn-clipboard {
border: 1px solid $border-color;
@@ -266,8 +221,7 @@
}
}
-table.u2f-registrations,
-.webauthn-registrations {
+table.u2f-registrations {
th:not(:last-child),
td:not(:last-child) {
border-right: solid 1px transparent;
diff --git a/app/assets/stylesheets/pages/profiles/preferences.scss b/app/assets/stylesheets/pages/profiles/preferences.scss
index 12386fa66e..b583d40de7 100644
--- a/app/assets/stylesheets/pages/profiles/preferences.scss
+++ b/app/assets/stylesheets/pages/profiles/preferences.scss
@@ -1,19 +1,3 @@
-.multi-file-editor-options {
- label {
- margin-right: 20px;
- text-align: center;
- }
-
- .preview {
- font-size: 0;
-
- img {
- border: 1px solid $border-color-settings;
- border-radius: 4px;
- }
- }
-}
-
.application-theme {
$ui-dark-bg: #2e2e2e;
$ui-light-bg: #dfdfdf;
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index c330e0709a..6b4d7c2520 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -622,12 +622,6 @@ pre.light-well {
}
}
- .star-button {
- .icon {
- top: 0;
- }
- }
-
.icon-container {
@include media-breakpoint-down(xs) {
margin-right: $gl-padding-8;
@@ -938,16 +932,6 @@ pre.light-well {
}
}
-.project-ci-linter {
- .ci-editor {
- height: 400px;
- }
-
- .ci-template pre {
- white-space: pre-wrap;
- }
-}
-
.project-badge {
opacity: 0.9;
diff --git a/app/assets/stylesheets/pages/prometheus.scss b/app/assets/stylesheets/pages/prometheus.scss
index a3b6cbdff2..71cbd7d961 100644
--- a/app/assets/stylesheets/pages/prometheus.scss
+++ b/app/assets/stylesheets/pages/prometheus.scss
@@ -96,11 +96,6 @@
padding: $gl-padding-8;
}
-.prometheus-graph-embed {
- border: 1px solid $border-color;
- border-radius: $border-radius-default;
-}
-
.alert-current-setting {
max-width: 240px;
@@ -110,233 +105,6 @@
}
}
-.prometheus-graph-cursor {
- position: absolute;
- background: $gray-400;
- width: 1px;
-}
-
-.prometheus-graph-flag {
- display: block;
- min-width: 160px;
- border: 0;
- box-shadow: 0 1px 4px 0 $black-transparent;
-
- h5 {
- padding: 0;
- margin: 0;
- font-size: 14px;
- line-height: 1.2;
- }
-
- .deploy-meta-content {
- border-bottom: 1px solid $white-dark;
-
- svg {
- height: 15px;
- vertical-align: bottom;
- }
- }
-
- &.popover {
- padding: 0;
-
- &.left {
- left: auto;
- right: 0;
- margin-right: 10px;
-
- > .arrow {
- right: -14px;
- border-left-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-left: 4px solid $gray-10;
- }
-
- .arrow-shadow {
- right: -3px;
- box-shadow: 1px 0 9px 0 $black-transparent;
- }
- }
-
- &.right {
- left: 0;
- right: auto;
- margin-left: 10px;
-
- > .arrow {
- left: -7px;
- border-right-color: $border-color;
- }
-
- > .arrow::after {
- border-top: 6px solid transparent;
- border-bottom: 6px solid transparent;
- border-right: 4px solid $gray-10;
- }
-
- .arrow-shadow {
- left: -3px;
- box-shadow: 1px 0 8px 0 $black-transparent;
- }
- }
-
- > .arrow {
- top: 10px;
- margin: 0;
- }
-
- .arrow-shadow {
- content: '';
- position: absolute;
- width: 7px;
- height: 7px;
- background-color: transparent;
- transform: rotate(45deg);
- top: 13px;
- }
-
- > .popover-title,
- > .popover-content,
- > .popover-header,
- > .popover-body {
- padding: 8px;
- white-space: nowrap;
- position: relative;
- }
-
- > .popover-title {
- background-color: $gray-10;
- border-radius: $border-radius-default $border-radius-default 0 0;
- }
- }
-
- strong {
- font-weight: 600;
- }
-}
-
-.prometheus-table {
- border-collapse: collapse;
- padding: 0;
- margin: 0;
-
- td {
- vertical-align: middle;
-
- + td {
- padding-left: 8px;
- vertical-align: top;
- }
- }
-
- .legend-metric-title {
- font-size: 12px;
- vertical-align: middle;
- }
-}
-
-.prometheus-svg-container {
- position: relative;
- height: 0;
- width: 100%;
- padding: 0;
- padding-bottom: 100%;
-
- .text-metric-usage {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 12px;
- }
-
- > svg {
- position: absolute;
- height: 100%;
- width: 100%;
- left: 0;
- top: 0;
-
- text {
- fill: $gl-text-color;
- stroke-width: 0;
- }
-
- .text-metric-bold {
- font-weight: $gl-font-weight-bold;
- }
-
- .label-axis-text {
- fill: $black;
- font-weight: $gl-font-weight-normal;
- font-size: 10px;
- }
-
- .legend-axis-text {
- fill: $black;
- }
-
- .tick {
- > line {
- stroke: $gray-darker;
- }
-
- > text {
- fill: $gray-400;
- font-size: 10px;
- }
- }
-
- .y-label-text,
- .x-label-text {
- fill: $gray-darkest;
- }
-
- .axis-tick {
- stroke: $gray-darker;
- }
-
- .deploy-info-text {
- dominant-baseline: text-before-edge;
- font-size: 12px;
- }
-
- .deploy-info-text-link {
- font-family: $monospace-font;
- fill: $blue-600;
-
- &:hover {
- fill: $blue-800;
- }
- }
-
- @include media-breakpoint-down(sm) {
- .label-axis-text,
- .text-metric-usage,
- .legend-axis-text {
- font-size: 8px;
- }
-
- .tick > text {
- font-size: 8px;
- }
- }
- }
-}
-
-.prometheus-table-row-highlight {
- background-color: $gray-100;
-}
-
-.prometheus-graph-overlay {
- fill: none;
- opacity: 0;
- pointer-events: all;
-}
-
.prometheus-panel-builder {
.preview-date-time-picker {
// same as in .dropdown-menu-toggle
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index aa9ebfe296..37e272cfff 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -154,17 +154,6 @@
}
}
-.token-token-container {
- #impersonation-token-token {
- width: 80%;
- display: inline;
- }
-
- .btn-clipboard {
- margin-left: 5px;
- }
-}
-
.visibility-level-setting {
.form-check {
margin-bottom: 10px;
@@ -203,22 +192,6 @@
}
}
-.initialize-with-readme-setting {
- .form-check {
- margin-bottom: 10px;
-
- .option-title {
- font-weight: $gl-font-weight-normal;
- display: inline-block;
- color: $gl-text-color;
- }
-
- .option-description {
- color: $project-option-descr-color;
- }
- }
-}
-
.nested-settings {
padding-left: 20px;
}
@@ -326,10 +299,6 @@
}
}
-.personal-access-tokens-never-expires-label {
- color: $note-disabled-comment-color;
-}
-
.created-deploy-token-container {
.deploy-token-field {
width: 90%;
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 5765156f26..33c6664871 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -223,11 +223,6 @@
min-height: 200px;
}
-.upload-link {
- font-weight: $gl-font-weight-normal;
- color: $blue-600;
-}
-
.repo-charts {
.sub-header {
margin: 20px 0;
diff --git a/app/assets/stylesheets/startup/startup-dark.scss b/app/assets/stylesheets/startup/startup-dark.scss
index b7958cdf4a..d436c32892 100644
--- a/app/assets/stylesheets/startup/startup-dark.scss
+++ b/app/assets/stylesheets/startup/startup-dark.scss
@@ -5,20 +5,17 @@
body.gl-dark {
--gray-50: #303030;
--gray-100: #404040;
+ --gray-900: #fafafa;
--gray-950: #fff;
--green-100: #0d532a;
--green-400: #108548;
--green-700: #91d4a8;
--blue-400: #1f75cb;
--orange-400: #ab6100;
- --purple-100: #2f2a6b;
--gl-text-color: #fafafa;
--border-color: #4f4f4f;
--black: #fff;
}
-.nav-sidebar li.active {
- box-shadow: none;
-}
:root {
--white: #333;
}
@@ -198,22 +195,6 @@ h1 {
.dropdown {
position: relative;
}
-.dropdown-menu-toggle {
- white-space: nowrap;
-}
-.dropdown-menu-toggle::after {
- display: inline-block;
- margin-left: 0.255em;
- vertical-align: 0.255em;
- content: "";
- border-top: 0.3em solid;
- border-right: 0.3em solid transparent;
- border-bottom: 0;
- border-left: 0.3em solid transparent;
-}
-.dropdown-menu-toggle:empty::after {
- margin-left: 0;
-}
.dropdown-menu {
position: absolute;
top: 100%;
@@ -331,6 +312,9 @@ h1 {
padding-left: 0.6em;
border-radius: 10rem;
}
+.bg-transparent {
+ background-color: transparent !important;
+}
.rounded-circle {
border-radius: 50% !important;
}
@@ -375,6 +359,20 @@ h1 {
.m-auto {
margin: auto !important;
}
+.gl-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+.gl-button .gl-badge {
+ top: 0;
+}
.gl-form-input,
.gl-form-input.form-control {
background-color: #333;
@@ -466,9 +464,6 @@ a {
.hide {
display: none;
}
-.dropdown-menu-toggle::after {
- display: none;
-}
.badge:not(.gl-badge) {
padding: 4px 5px;
font-size: 12px;
@@ -548,7 +543,7 @@ body {
border-radius: 0.25rem;
white-space: nowrap;
}
-.no-outline.dropdown-menu-toggle {
+.dropdown-menu-toggle.no-outline {
outline: 0;
}
.dropdown-menu-toggle.dropdown-menu-toggle {
@@ -875,6 +870,12 @@ input {
.navbar-nav .badge.badge-pill:not(.merge-request-badge).todos-count {
background-color: var(--blue-400, #1f75cb);
}
+.title-container .canary-badge .badge,
+.navbar-nav .canary-badge .badge {
+ font-size: 12px;
+ line-height: 16px;
+ padding: 0 0.5rem;
+}
@media (max-width: 575.98px) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@@ -1280,6 +1281,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
+ border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
@@ -1304,6 +1306,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
+ border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items > li .badge.badge-pill {
@@ -1601,19 +1604,98 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
-.rect-avatar.s32,
-.nav-sidebar-inner-scroll
- > div.context-header
- a
- .avatar-container.rect-avatar
- .avatar.s32,
-.sidebar-top-level-items
- .context-header
- a
- .avatar-container.rect-avatar
- .avatar.s32 {
+.rect-avatar.s32 {
border-radius: 4px;
}
+body.gl-dark {
+ --gray-10: #1f1f1f;
+ --gray-50: #303030;
+ --gray-100: #404040;
+ --gray-200: #525252;
+ --gray-300: #5e5e5e;
+ --gray-400: #868686;
+ --gray-500: #999;
+ --gray-600: #bfbfbf;
+ --gray-700: #dbdbdb;
+ --gray-800: #f0f0f0;
+ --gray-900: #fafafa;
+ --gray-950: #fff;
+ --green-50: #0a4020;
+ --green-100: #0d532a;
+ --green-200: #24663b;
+ --green-300: #217645;
+ --green-400: #108548;
+ --green-500: #2da160;
+ --green-600: #52b87a;
+ --green-700: #91d4a8;
+ --green-800: #c3e6cd;
+ --green-900: #ecf4ee;
+ --green-950: #f1fdf6;
+ --blue-50: #033464;
+ --blue-100: #064787;
+ --blue-200: #0b5cad;
+ --blue-300: #1068bf;
+ --blue-400: #1f75cb;
+ --blue-500: #428fdc;
+ --blue-600: #63a6e9;
+ --blue-700: #9dc7f1;
+ --blue-800: #cbe2f9;
+ --blue-900: #e9f3fc;
+ --blue-950: #f2f9ff;
+ --orange-50: #5c2900;
+ --orange-100: #703800;
+ --orange-200: #8f4700;
+ --orange-300: #9e5400;
+ --orange-400: #ab6100;
+ --orange-500: #c17d10;
+ --orange-600: #d99530;
+ --orange-700: #e9be74;
+ --orange-800: #f5d9a8;
+ --orange-900: #fdf1dd;
+ --orange-950: #fff4e1;
+ --red-50: #660e00;
+ --red-100: #8d1300;
+ --red-200: #ae1800;
+ --red-300: #c91c00;
+ --red-400: #dd2b0e;
+ --red-500: #ec5941;
+ --red-600: #f57f6c;
+ --red-700: #fcb5aa;
+ --red-800: #fdd4cd;
+ --red-900: #fcf1ef;
+ --red-950: #fff4f3;
+ --indigo-50: #1a1a40;
+ --indigo-100: #292961;
+ --indigo-200: #393982;
+ --indigo-300: #4b4ba3;
+ --indigo-400: #5b5bbd;
+ --indigo-500: #6666c4;
+ --indigo-600: #7c7ccc;
+ --indigo-700: #a6a6de;
+ --indigo-800: #d1d1f0;
+ --indigo-900: #ebebfa;
+ --indigo-950: #f7f7ff;
+ --indigo-900-alpha-008: rgba(235, 235, 250, 0.08);
+ --purple-50: #232150;
+ --purple-100: #2f2a6b;
+ --purple-200: #453894;
+ --purple-300: #5943b6;
+ --purple-400: #694cc0;
+ --purple-500: #7b58cf;
+ --purple-600: #9475db;
+ --purple-700: #ac93e6;
+ --purple-800: #cbbbf2;
+ --purple-900: #e1d8f9;
+ --purple-950: #f4f0ff;
+ --gl-text-color: #fafafa;
+ --border-color: #4f4f4f;
+ --white: #333;
+ --black: #fff;
+ --svg-status-bg: #333;
+}
+.nav-sidebar li.active {
+ box-shadow: none;
+}
body.gl-dark .navbar-gitlab {
background-color: #fafafa;
}
@@ -1703,8 +1785,8 @@ body.gl-dark .nav-sidebar li.active > a {
body.gl-dark .nav-sidebar .fly-out-top-item a,
body.gl-dark .nav-sidebar .fly-out-top-item.active a,
body.gl-dark .nav-sidebar .fly-out-top-item .fly-out-top-item-container {
- background-color: var(--purple-100, #e1d8f9);
- color: var(--black, #333);
+ background-color: var(--gray-100, #303030);
+ color: var(--gray-900, #fafafa);
}
body.gl-dark .logo-text svg {
fill: var(--gl-text-color);
@@ -1823,9 +1905,6 @@ body.gl-dark {
--black: #fff;
--svg-status-bg: #333;
}
-.nav-sidebar li.active {
- box-shadow: none;
-}
.tab-width-8 {
-moz-tab-size: 8;
tab-size: 8;
@@ -1841,9 +1920,18 @@ body.gl-dark {
white-space: nowrap;
width: 1px;
}
+.gl-bg-green-500 {
+ background-color: #2da160;
+}
.gl-border-none\! {
border-style: none !important;
}
+.gl-rounded-pill {
+ border-radius: 0.75rem;
+}
+.gl-text-white {
+ color: #333;
+}
.gl-display-none {
display: none;
}
@@ -1862,6 +1950,10 @@ body.gl-dark {
.gl-pr-2 {
padding-right: 0.25rem;
}
+.gl-py-1 {
+ padding-top: 0.125rem;
+ padding-bottom: 0.125rem;
+}
.gl-ml-3 {
margin-left: 0.5rem;
}
diff --git a/app/assets/stylesheets/startup/startup-general.scss b/app/assets/stylesheets/startup/startup-general.scss
index 2c79b81989..40026c95a1 100644
--- a/app/assets/stylesheets/startup/startup-general.scss
+++ b/app/assets/stylesheets/startup/startup-general.scss
@@ -178,22 +178,6 @@ h1 {
.dropdown {
position: relative;
}
-.dropdown-menu-toggle {
- white-space: nowrap;
-}
-.dropdown-menu-toggle::after {
- display: inline-block;
- margin-left: 0.255em;
- vertical-align: 0.255em;
- content: "";
- border-top: 0.3em solid;
- border-right: 0.3em solid transparent;
- border-bottom: 0;
- border-left: 0.3em solid transparent;
-}
-.dropdown-menu-toggle:empty::after {
- margin-left: 0;
-}
.dropdown-menu {
position: absolute;
top: 100%;
@@ -311,6 +295,9 @@ h1 {
padding-left: 0.6em;
border-radius: 10rem;
}
+.bg-transparent {
+ background-color: transparent !important;
+}
.rounded-circle {
border-radius: 50% !important;
}
@@ -355,6 +342,20 @@ h1 {
.m-auto {
margin: auto !important;
}
+.gl-badge {
+ display: inline-flex;
+ align-items: center;
+ font-size: 0.75rem;
+ font-weight: 400;
+ line-height: 1rem;
+ padding-top: 0.25rem;
+ padding-bottom: 0.25rem;
+ padding-left: 0.5rem;
+ padding-right: 0.5rem;
+}
+.gl-button .gl-badge {
+ top: 0;
+}
.gl-form-input,
.gl-form-input.form-control {
background-color: #fff;
@@ -446,9 +447,6 @@ a {
.hide {
display: none;
}
-.dropdown-menu-toggle::after {
- display: none;
-}
.badge:not(.gl-badge) {
padding: 4px 5px;
font-size: 12px;
@@ -528,7 +526,7 @@ body {
border-radius: 0.25rem;
white-space: nowrap;
}
-.no-outline.dropdown-menu-toggle {
+.dropdown-menu-toggle.no-outline {
outline: 0;
}
.dropdown-menu-toggle.dropdown-menu-toggle {
@@ -855,6 +853,12 @@ input {
.navbar-nav .badge.badge-pill:not(.merge-request-badge).todos-count {
background-color: var(--blue-400, #428fdc);
}
+.title-container .canary-badge .badge,
+.navbar-nav .canary-badge .badge {
+ font-size: 12px;
+ line-height: 16px;
+ padding: 0 0.5rem;
+}
@media (max-width: 575.98px) {
.navbar-gitlab .container-fluid {
font-size: 18px;
@@ -1260,6 +1264,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
+ border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items {
@@ -1284,6 +1289,7 @@ input {
a
.avatar-container.rect-avatar
.avatar.s32 {
+ border-radius: 4px;
box-shadow: inset 0 0 0 1px rgba(0, 0, 0, 0.08);
}
.sidebar-top-level-items > li .badge.badge-pill {
@@ -1581,17 +1587,7 @@ svg.s16 {
.rect-avatar.s16 {
border-radius: 2px;
}
-.rect-avatar.s32,
-.nav-sidebar-inner-scroll
- > div.context-header
- a
- .avatar-container.rect-avatar
- .avatar.s32,
-.sidebar-top-level-items
- .context-header
- a
- .avatar-container.rect-avatar
- .avatar.s32 {
+.rect-avatar.s32 {
border-radius: 4px;
}
@@ -1610,9 +1606,18 @@ svg.s16 {
white-space: nowrap;
width: 1px;
}
+.gl-bg-green-500 {
+ background-color: #108548;
+}
.gl-border-none\! {
border-style: none !important;
}
+.gl-rounded-pill {
+ border-radius: 0.75rem;
+}
+.gl-text-white {
+ color: #fff;
+}
.gl-display-none {
display: none;
}
@@ -1631,6 +1636,10 @@ svg.s16 {
.gl-pr-2 {
padding-right: 0.25rem;
}
+.gl-py-1 {
+ padding-top: 0.125rem;
+ padding-bottom: 0.125rem;
+}
.gl-ml-3 {
margin-left: 0.5rem;
}
diff --git a/app/assets/stylesheets/startup/startup-signin.scss b/app/assets/stylesheets/startup/startup-signin.scss
index 013ad3fac8..8d7531d6c9 100644
--- a/app/assets/stylesheets/startup/startup-signin.scss
+++ b/app/assets/stylesheets/startup/startup-signin.scss
@@ -258,21 +258,6 @@ fieldset:disabled a.btn {
align-items: center;
justify-content: space-between;
}
-.d-block {
- display: block !important;
-}
-.d-flex {
- display: flex !important;
-}
-.flex-wrap {
- flex-wrap: wrap !important;
-}
-.justify-content-between {
- justify-content: space-between !important;
-}
-.align-items-center {
- align-items: center !important;
-}
.fixed-top {
position: fixed;
top: 0;
@@ -280,9 +265,6 @@ fieldset:disabled a.btn {
left: 0;
z-index: 1030;
}
-.ml-2 {
- margin-left: 0.5rem !important;
-}
.mt-3 {
margin-top: 1rem !important;
}
@@ -349,6 +331,15 @@ fieldset:disabled a.btn {
font-size: 0.875rem;
border-radius: 0.25rem;
}
+.gl-button.gl-button .gl-button-text {
+ overflow: hidden;
+ text-overflow: ellipsis;
+ white-space: nowrap;
+ padding-top: 1px;
+ padding-bottom: 1px;
+ margin-top: -1px;
+ margin-bottom: -1px;
+}
.gl-button.gl-button .gl-button-icon {
height: 1rem;
width: 1rem;
@@ -637,10 +628,6 @@ svg {
padding: 0;
border: 0;
background: none;
- margin-bottom: 16px;
-}
-.login-page .omniauth-container .omniauth-btn {
- width: 100%;
}
.login-page .new-session-tabs {
display: flex;
@@ -771,21 +758,18 @@ svg {
.gl-align-items-center {
align-items: center;
}
+.gl-flex-wrap {
+ flex-wrap: wrap;
+}
.gl-w-full {
width: 100%;
}
-.gl-p-2 {
- padding: 0.25rem;
-}
.gl-p-4 {
padding: 0.75rem;
}
.gl-mt-2 {
margin-top: 0.25rem;
}
-.gl-mb-2 {
- margin-bottom: 0.25rem;
-}
.gl-mb-3 {
margin-bottom: 0.5rem;
}
@@ -797,8 +781,8 @@ svg {
margin-top: 0;
}
}
-.gl-text-left {
- text-align: left;
+.gl-font-weight-bold {
+ font-weight: 600;
}
@import "startup/cloaking";
diff --git a/app/assets/stylesheets/themes/_dark.scss b/app/assets/stylesheets/themes/_dark.scss
index f12b2ee259..c79816e357 100644
--- a/app/assets/stylesheets/themes/_dark.scss
+++ b/app/assets/stylesheets/themes/_dark.scss
@@ -202,7 +202,8 @@ body.gl-dark {
&.btn-info,
&.btn-success,
&.btn-danger,
- &.btn-warning {
+ &.btn-warning,
+ &.btn-confirm {
&-tertiary {
mix-blend-mode: screen;
}
@@ -256,53 +257,3 @@ $line-removed-dark: $red-200;
$well-expand-item: $gray-200;
$well-inner-border: $gray-200;
-
-// Misc component overrides that should live elsewhere
-.gl-label {
- filter: brightness(0.9) contrast(1.1);
-
- // This applies to the gl-label markups
- // rendered and cached in the backend (labels_helper.rb)
- &.gl-label-scoped {
- .gl-label-text-scoped,
- .gl-label-close {
- color: $gray-900;
- }
- }
-}
-
-// white-ish text for light labels
-.gl-label-text-light.gl-label-text-light {
- color: $gray-900;
-}
-
-.gl-label-text-dark.gl-label-text-dark {
- color: $gray-10;
-}
-
-// This applies to "gl-labels" from "gitlab-ui"
-.gl-label.gl-label-scoped.gl-label-text-dark,
-.gl-label.gl-label-scoped.gl-label-text-light {
- .gl-label-text-scoped,
- .gl-label-close {
- color: $gray-900;
- }
-}
-
-// duplicated class as the original .atwho-view style is added later
-.atwho-view.atwho-view {
- background-color: $white;
- color: $gray-900;
- border-color: $gray-800;
-}
-
-.nav-sidebar {
- li.active {
- box-shadow: none;
- }
-
- .sidebar-sub-level-items.fly-out-list {
- box-shadow: none;
- border: 1px solid $border-color;
- }
-}
diff --git a/app/assets/stylesheets/themes/dark_mode_overrides.scss b/app/assets/stylesheets/themes/dark_mode_overrides.scss
new file mode 100644
index 0000000000..b77048174c
--- /dev/null
+++ b/app/assets/stylesheets/themes/dark_mode_overrides.scss
@@ -0,0 +1,116 @@
+@import './themes/dark';
+@import 'page_bundles/mixins_and_variables_and_functions';
+@import './themes/theme_helper';
+
+// Some hacks and overrides for things that don't properly support dark mode
+.gl-label {
+ filter: brightness(0.9) contrast(1.1);
+
+ // This applies to the gl-label markups
+ // rendered and cached in the backend (labels_helper.rb)
+ &.gl-label-scoped {
+ .gl-label-text-scoped,
+ .gl-label-close {
+ color: $gray-900;
+ }
+ }
+}
+
+// white-ish text for light labels
+.gl-label-text-light.gl-label-text-light {
+ color: $gray-900;
+}
+
+.gl-label-text-dark.gl-label-text-dark {
+ color: $gray-10;
+}
+
+// This applies to "gl-labels" from "gitlab-ui"
+.gl-label.gl-label-scoped.gl-label-text-dark,
+.gl-label.gl-label-scoped.gl-label-text-light {
+ .gl-label-text-scoped,
+ .gl-label-close {
+ color: $gray-900;
+ }
+}
+
+// duplicated class as the original .atwho-view style is added later
+.atwho-view.atwho-view {
+ background-color: $white;
+ color: $gray-900;
+ border-color: $gray-800;
+}
+
+.nav-sidebar {
+ li.active {
+ box-shadow: none;
+ }
+
+ .sidebar-sub-level-items.fly-out-list {
+ box-shadow: none;
+ border: 1px solid $border-color;
+ }
+}
+
+body.gl-dark {
+ @include gitlab-theme($gray-900, $gray-400, $gray-500, $gray-800, $gray-900, $white);
+
+ .logo-text svg {
+ fill: var(--gl-text-color);
+ }
+
+ .navbar-gitlab {
+ background-color: var(--gray-50);
+ box-shadow: 0 1px 0 0 var(--gray-100);
+
+ .navbar-sub-nav,
+ .navbar-nav {
+ li {
+ > a:hover,
+ > a:focus,
+ > button:hover,
+ > button:focus {
+ color: var(--gl-text-color);
+ background-color: var(--gray-200);
+ }
+ }
+
+ li.active,
+ li.dropdown.show {
+ > a,
+ > button {
+ color: var(--gl-text-color);
+ background-color: var(--gray-200);
+ }
+ }
+ }
+
+ .header-search {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--border-color) !important;
+
+ &:active,
+ &:hover {
+ background-color: var(--gray-100) !important;
+ box-shadow: inset 0 0 0 1px var(--blue-200) !important;
+ }
+ }
+
+ .search {
+ form {
+ background-color: var(--gray-100);
+ box-shadow: inset 0 0 0 1px var(--border-color);
+
+ &:active,
+ &:hover {
+ background-color: var(--gray-100);
+ box-shadow: inset 0 0 0 1px var(--blue-200);
+ }
+ }
+ }
+ }
+
+ .md :not(pre.code) > code {
+ background-color: $gray-200;
+ }
+}
diff --git a/app/assets/stylesheets/themes/theme_helper.scss b/app/assets/stylesheets/themes/theme_helper.scss
index a9e8b238d7..1332686a90 100644
--- a/app/assets/stylesheets/themes/theme_helper.scss
+++ b/app/assets/stylesheets/themes/theme_helper.scss
@@ -212,8 +212,8 @@
a:hover,
&.active a,
.fly-out-top-item-container {
- background-color: var(--purple-100, $purple-900);
- color: var(--black, $white);
+ background-color: var(--gray-100, $gray-50);
+ color: var(--gray-900, $gray-900);
}
}
}
diff --git a/app/controllers/admin/dashboard_controller.rb b/app/controllers/admin/dashboard_controller.rb
index ba24e3e619..d12ccfc742 100644
--- a/app/controllers/admin/dashboard_controller.rb
+++ b/app/controllers/admin/dashboard_controller.rb
@@ -15,7 +15,14 @@ class Admin::DashboardController < Admin::ApplicationController
@groups = Group.order_id_desc.with_route.limit(10)
@notices = Gitlab::ConfigChecker::PumaRuggedChecker.check
@notices += Gitlab::ConfigChecker::ExternalDatabaseChecker.check
- @redis_versions = [Gitlab::Redis::Queues, Gitlab::Redis::SharedState, Gitlab::Redis::Cache, Gitlab::Redis::TraceChunks].map(&:version).uniq
+ @redis_versions = [
+ Gitlab::Redis::Queues,
+ Gitlab::Redis::SharedState,
+ Gitlab::Redis::Cache,
+ Gitlab::Redis::TraceChunks,
+ Gitlab::Redis::RateLimiting,
+ Gitlab::Redis::Sessions
+ ].map(&:version).uniq
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/admin/instance_review_controller.rb b/app/controllers/admin/instance_review_controller.rb
index 88ca2c88aa..5567ffbdc8 100644
--- a/app/controllers/admin/instance_review_controller.rb
+++ b/app/controllers/admin/instance_review_controller.rb
@@ -3,7 +3,7 @@ class Admin::InstanceReviewController < Admin::ApplicationController
feature_category :devops_reports
def index
- redirect_to("#{::Gitlab::SubscriptionPortal::SUBSCRIPTIONS_URL}/instance_review?#{instance_review_params}")
+ redirect_to("#{Gitlab::SubscriptionPortal.subscriptions_instance_review_url}?#{instance_review_params}")
end
def instance_review_params
diff --git a/app/controllers/admin/serverless/domains_controller.rb b/app/controllers/admin/serverless/domains_controller.rb
deleted file mode 100644
index 49cd9f7a36..0000000000
--- a/app/controllers/admin/serverless/domains_controller.rb
+++ /dev/null
@@ -1,78 +0,0 @@
-# frozen_string_literal: true
-
-class Admin::Serverless::DomainsController < Admin::ApplicationController
- before_action :check_feature_flag
- before_action :domain, only: [:update, :verify, :destroy]
-
- feature_category :serverless
-
- def index
- @domain = PagesDomain.instance_serverless.first_or_initialize
- end
-
- def create
- if PagesDomain.instance_serverless.exists?
- return redirect_to admin_serverless_domains_path, notice: _('An instance-level serverless domain already exists.')
- end
-
- @domain = PagesDomain.instance_serverless.create(create_params)
-
- if @domain.persisted?
- redirect_to admin_serverless_domains_path, notice: _('Domain was successfully created.')
- else
- render 'index'
- end
- end
-
- def update
- if domain.update(update_params)
- redirect_to admin_serverless_domains_path, notice: _('Domain was successfully updated.')
- else
- render 'index'
- end
- end
-
- def destroy
- if domain.serverless_domain_clusters.exists?
- return redirect_to admin_serverless_domains_path,
- status: :conflict,
- notice: _('Domain cannot be deleted while associated to one or more clusters.')
- end
-
- domain.destroy!
-
- redirect_to admin_serverless_domains_path,
- status: :found,
- notice: _('Domain was successfully deleted.')
- end
-
- def verify
- result = VerifyPagesDomainService.new(domain).execute
-
- if result[:status] == :success
- flash[:notice] = _('Successfully verified domain ownership')
- else
- flash[:alert] = _('Failed to verify domain ownership')
- end
-
- redirect_to admin_serverless_domains_path
- end
-
- private
-
- def domain
- @domain = PagesDomain.instance_serverless.find(params[:id])
- end
-
- def check_feature_flag
- render_404 unless Feature.enabled?(:serverless_domain)
- end
-
- def update_params
- params.require(:pages_domain).permit(:user_provided_certificate, :user_provided_key)
- end
-
- def create_params
- params.require(:pages_domain).permit(:domain, :user_provided_certificate, :user_provided_key)
- end
-end
diff --git a/app/controllers/admin/topics/avatars_controller.rb b/app/controllers/admin/topics/avatars_controller.rb
new file mode 100644
index 0000000000..7acdec424b
--- /dev/null
+++ b/app/controllers/admin/topics/avatars_controller.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+class Admin::Topics::AvatarsController < Admin::ApplicationController
+ feature_category :projects
+
+ def destroy
+ @topic = Projects::Topic.find(params[:topic_id])
+
+ @topic.remove_avatar!
+ @topic.save
+
+ redirect_to edit_admin_topic_path(@topic), status: :found
+ end
+end
diff --git a/app/controllers/admin/topics_controller.rb b/app/controllers/admin/topics_controller.rb
new file mode 100644
index 0000000000..ccc38ba7cd
--- /dev/null
+++ b/app/controllers/admin/topics_controller.rb
@@ -0,0 +1,57 @@
+# frozen_string_literal: true
+
+class Admin::TopicsController < Admin::ApplicationController
+ include SendFileUpload
+ include PreviewMarkdown
+
+ before_action :topic, only: [:edit, :update]
+
+ feature_category :projects
+
+ def index
+ @topics = Projects::TopicsFinder.new(params: params.permit(:search)).execute.page(params[:page]).without_count
+ end
+
+ def new
+ @topic = Projects::Topic.new
+ end
+
+ def edit
+ end
+
+ def create
+ @topic = Projects::Topic.new(topic_params)
+
+ if @topic.save
+ redirect_to edit_admin_topic_path(@topic), notice: _('Topic %{topic_name} was successfully created.') % { topic_name: @topic.name }
+ else
+ render "new"
+ end
+ end
+
+ def update
+ if @topic.update(topic_params)
+ redirect_to edit_admin_topic_path(@topic), notice: _('Topic was successfully updated.')
+ else
+ render "edit"
+ end
+ end
+
+ private
+
+ def topic
+ @topic ||= Projects::Topic.find(params[:id])
+ end
+
+ def topic_params
+ params.require(:projects_topic).permit(allowed_topic_params)
+ end
+
+ def allowed_topic_params
+ [
+ :avatar,
+ :description,
+ :name
+ ]
+ end
+end
diff --git a/app/controllers/application_controller.rb b/app/controllers/application_controller.rb
index a83458f326..b22167a395 100644
--- a/app/controllers/application_controller.rb
+++ b/app/controllers/application_controller.rb
@@ -21,7 +21,7 @@ class ApplicationController < ActionController::Base
include Impersonation
include Gitlab::Logging::CloudflareHelper
include Gitlab::Utils::StrongMemoize
- include ::Gitlab::WithFeatureCategory
+ include ::Gitlab::EndpointAttributes
include FlocOptOut
before_action :authenticate_user!, except: [:route_not_found]
@@ -70,6 +70,10 @@ class ApplicationController < ActionController::Base
# concerns due to caching private data.
DEFAULT_GITLAB_CACHE_CONTROL = "#{ActionDispatch::Http::Cache::Response::DEFAULT_CACHE_CONTROL}, no-store"
+ def self.endpoint_id_for_action(action_name)
+ "#{self.name}##{action_name}"
+ end
+
rescue_from Encoding::CompatibilityError do |exception|
log_exception(exception)
render "errors/encoding", layout: "errors", status: :internal_server_error
@@ -104,6 +108,12 @@ class ApplicationController < ActionController::Base
head :forbidden, retry_after: Gitlab::Auth::UniqueIpsLimiter.config.unique_ips_limit_time_window
end
+ rescue_from RateLimitedService::RateLimitedError do |e|
+ e.log_request(request, current_user)
+ response.headers.merge!(e.headers)
+ render plain: e.message, status: :too_many_requests
+ end
+
def redirect_back_or_default(default: root_path, options: {})
redirect_back(fallback_location: default, **options)
end
@@ -131,6 +141,14 @@ class ApplicationController < ActionController::Base
end
end
+ def feature_category
+ self.class.feature_category_for_action(action_name).to_s
+ end
+
+ def urgency
+ self.class.urgency_for_action(action_name)
+ end
+
protected
def workhorse_excluded_content_types
@@ -457,7 +475,7 @@ class ApplicationController < ActionController::Base
user: -> { context_user },
project: -> { @project if @project&.persisted? },
namespace: -> { @group if @group&.persisted? },
- caller_id: caller_id,
+ caller_id: self.class.endpoint_id_for_action(action_name),
remote_ip: request.ip,
feature_category: feature_category
)
@@ -543,14 +561,6 @@ class ApplicationController < ActionController::Base
auth_user if strong_memoized?(:auth_user)
end
- def caller_id
- "#{self.class.name}##{action_name}"
- end
-
- def feature_category
- self.class.feature_category_for_action(action_name).to_s
- end
-
def required_signup_info
return unless current_user
return unless current_user.role_required?
diff --git a/app/controllers/concerns/group_tree.rb b/app/controllers/concerns/group_tree.rb
index d076c62c70..35c1f358a7 100644
--- a/app/controllers/concerns/group_tree.rb
+++ b/app/controllers/concerns/group_tree.rb
@@ -38,8 +38,13 @@ module GroupTree
#
# Pagination needs to be applied before loading the ancestors to
# make sure ancestors are not cut off by pagination.
- Gitlab::ObjectHierarchy.new(Group.where(id: filtered_groups.select(:id)))
- .base_and_ancestors
+ filtered_groups_relation = Group.where(id: filtered_groups.select(:id))
+
+ if Feature.enabled?(:linear_group_tree_ancestor_scopes, current_user, default_enabled: :yaml)
+ filtered_groups_relation.self_and_ancestors
+ else
+ Gitlab::ObjectHierarchy.new(filtered_groups_relation).base_and_ancestors
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 7ee680db7f..e1e662a196 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -158,8 +158,10 @@ module IssuableActions
discussions = Discussion.build_collection(notes, issuable)
- if issuable.is_a?(MergeRequest) && Feature.enabled?(:merge_request_discussion_cache, issuable.target_project, default_enabled: :yaml)
- render_cached(discussions, with: discussion_serializer, context: self)
+ if issuable.is_a?(MergeRequest)
+ cache_context = [current_user&.cache_key, project.team.human_max_access(current_user&.id)].join(':')
+
+ render_cached(discussions, with: discussion_serializer, cache_context: -> (_) { cache_context }, context: self)
else
render json: discussion_serializer.represent(discussions, context: self)
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 2d8168af2e..c2ee735a2b 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -62,7 +62,7 @@ module NotesActions
json.merge!(note_json(@note))
end
- if @note.errors.present? && @note.errors.keys != [:commands_only]
+ if @note.errors.present? && @note.errors.attribute_names != [:commands_only]
render json: json, status: :unprocessable_entity
else
render json: json
diff --git a/app/controllers/concerns/one_trust_csp.rb b/app/controllers/concerns/one_trust_csp.rb
new file mode 100644
index 0000000000..4e98ec586c
--- /dev/null
+++ b/app/controllers/concerns/one_trust_csp.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module OneTrustCSP
+ extend ActiveSupport::Concern
+
+ included do
+ content_security_policy do |policy|
+ next if policy.directives.blank?
+
+ default_script_src = policy.directives['script-src'] || policy.directives['default-src']
+ script_src_values = Array.wrap(default_script_src) | ["'unsafe-eval'", 'https://cdn.cookielaw.org https://*.onetrust.com']
+ policy.script_src(*script_src_values)
+
+ default_connect_src = policy.directives['connect-src'] || policy.directives['default-src']
+ connect_src_values = Array.wrap(default_connect_src) | ['https://cdn.cookielaw.org']
+ policy.connect_src(*connect_src_values)
+ end
+ end
+end
diff --git a/app/controllers/concerns/registry/connection_errors_handler.rb b/app/controllers/concerns/registry/connection_errors_handler.rb
new file mode 100644
index 0000000000..2b24f3b5b3
--- /dev/null
+++ b/app/controllers/concerns/registry/connection_errors_handler.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Registry
+ module ConnectionErrorsHandler
+ extend ActiveSupport::Concern
+
+ included do
+ rescue_from ContainerRegistry::Path::InvalidRegistryPathError, with: :invalid_registry_path
+ rescue_from Faraday::Error, with: :connection_error
+
+ before_action :ping_container_registry
+ end
+
+ private
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ # These instance variables are only read by a view helper to pass
+ # them to the frontend
+ # See app/views/projects/registry/repositories/index.html.haml
+ # app/views/groups/registry/repositories/index.html.haml
+ def invalid_registry_path
+ @invalid_path_error = true
+
+ render :index
+ end
+
+ def connection_error
+ @connection_error = true
+
+ render :index
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
+
+ def ping_container_registry
+ ContainerRegistry::Client.registry_info
+ end
+ end
+end
diff --git a/app/controllers/dashboard/projects_controller.rb b/app/controllers/dashboard/projects_controller.rb
index 74ad78ff4c..d861ef646f 100644
--- a/app/controllers/dashboard/projects_controller.rb
+++ b/app/controllers/dashboard/projects_controller.rb
@@ -36,7 +36,7 @@ class Dashboard::ProjectsController < Dashboard::ApplicationController
# rubocop: disable CodeReuse/ActiveRecord
def starred
@projects = load_projects(params.merge(starred: true))
- .includes(:forked_from_project, :topics, :topics_acts_as_taggable)
+ .includes(:forked_from_project, :topics)
@groups = []
diff --git a/app/controllers/graphql_controller.rb b/app/controllers/graphql_controller.rb
index 515fbd7b48..0722a712b5 100644
--- a/app/controllers/graphql_controller.rb
+++ b/app/controllers/graphql_controller.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GraphqlController < ApplicationController
+ extend ::Gitlab::Utils::Override
+
# Unauthenticated users have access to the API for public data
skip_before_action :authenticate_user!
@@ -35,6 +37,7 @@ class GraphqlController < ApplicationController
# callback execution order here
around_action :sessionless_bypass_admin_mode!, if: :sessionless_user?
+ # The default feature category is overridden to read from request
feature_category :not_owned
def execute
@@ -64,6 +67,11 @@ class GraphqlController < ApplicationController
render_error(exception.message, status: :unprocessable_entity)
end
+ override :feature_category
+ def feature_category
+ ::Gitlab::FeatureCategories.default.from_request(request) || super
+ end
+
private
def disallow_mutations_for_get
diff --git a/app/controllers/groups/boards_controller.rb b/app/controllers/groups/boards_controller.rb
index 60708c13b8..e8e6a7e5c1 100644
--- a/app/controllers/groups/boards_controller.rb
+++ b/app/controllers/groups/boards_controller.rb
@@ -11,6 +11,7 @@ class Groups::BoardsController < Groups::ApplicationController
push_frontend_feature_flag(:board_multi_select, group, default_enabled: :yaml)
push_frontend_feature_flag(:swimlanes_buffered_rendering, group, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, group, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/groups/dependency_proxy/application_controller.rb b/app/controllers/groups/dependency_proxy/application_controller.rb
index fd9db41f74..18a6ff93e1 100644
--- a/app/controllers/groups/dependency_proxy/application_controller.rb
+++ b/app/controllers/groups/dependency_proxy/application_controller.rb
@@ -21,8 +21,14 @@ module Groups
authenticate_with_http_token do |token, _|
@authentication_result = EMPTY_AUTH_RESULT
- found_user = user_from_token(token)
- sign_in(found_user) if found_user.is_a?(User)
+ user_or_deploy_token = ::DependencyProxy::AuthTokenService.user_or_deploy_token_from_jwt(token)
+
+ if user_or_deploy_token.is_a?(User)
+ @authentication_result = Gitlab::Auth::Result.new(user_or_deploy_token, nil, :user, [])
+ sign_in(user_or_deploy_token)
+ elsif user_or_deploy_token.is_a?(DeployToken)
+ @authentication_result = Gitlab::Auth::Result.new(user_or_deploy_token, nil, :deploy_token, [])
+ end
end
request_bearer_token! unless authenticated_user
@@ -39,28 +45,6 @@ module Groups
response.headers['WWW-Authenticate'] = ::DependencyProxy::Registry.authenticate_header
render plain: '', status: :unauthorized
end
-
- def user_from_token(token)
- token_payload = ::DependencyProxy::AuthTokenService.decoded_token_payload(token)
-
- if token_payload['user_id']
- token_user = User.find(token_payload['user_id'])
- return unless token_user
-
- @authentication_result = Gitlab::Auth::Result.new(token_user, nil, :user, [])
- return token_user
- elsif token_payload['deploy_token']
- deploy_token = DeployToken.active.find_by_token(token_payload['deploy_token'])
- return unless deploy_token
-
- @authentication_result = Gitlab::Auth::Result.new(deploy_token, nil, :deploy_token, [])
- return deploy_token
- end
-
- nil
- rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
- nil
- end
end
end
end
diff --git a/app/controllers/groups/dependency_proxy_for_containers_controller.rb b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
index f7dc552bd3..e19b8ae35f 100644
--- a/app/controllers/groups/dependency_proxy_for_containers_controller.rb
+++ b/app/controllers/groups/dependency_proxy_for_containers_controller.rb
@@ -5,11 +5,15 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
include DependencyProxy::GroupAccess
include SendFileUpload
include ::PackagesHelper # for event tracking
+ include WorkhorseRequest
before_action :ensure_group
- before_action :ensure_token_granted!
+ before_action :ensure_token_granted!, only: [:blob, :manifest]
before_action :ensure_feature_enabled!
+ before_action :verify_workhorse_api!, only: [:authorize_upload_blob, :upload_blob]
+ skip_before_action :verify_authenticity_token, only: [:authorize_upload_blob, :upload_blob]
+
attr_reader :token
feature_category :dependency_proxy
@@ -38,6 +42,8 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
def blob
+ return blob_via_workhorse if Feature.enabled?(:dependency_proxy_workhorse, group, default_enabled: :yaml)
+
result = DependencyProxy::FindOrCreateBlobService
.new(group, image, token, params[:sha]).execute
@@ -50,11 +56,47 @@ class Groups::DependencyProxyForContainersController < ::Groups::DependencyProxy
end
end
+ def authorize_upload_blob
+ set_workhorse_internal_api_content_type
+
+ render json: DependencyProxy::FileUploader.workhorse_authorize(has_length: false)
+ end
+
+ def upload_blob
+ @group.dependency_proxy_blobs.create!(
+ file_name: blob_file_name,
+ file: params[:file],
+ size: params[:file].size
+ )
+
+ event_name = tracking_event_name(object_type: :blob, from_cache: false)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ head :ok
+ end
+
private
+ def blob_via_workhorse
+ blob = @group.dependency_proxy_blobs.find_by_file_name(blob_file_name)
+
+ if blob.present?
+ event_name = tracking_event_name(object_type: :blob, from_cache: true)
+ track_package_event(event_name, :dependency_proxy, namespace: group, user: auth_user)
+
+ send_upload(blob.file)
+ else
+ send_dependency(token, DependencyProxy::Registry.blob_url(image, params[:sha]), blob_file_name)
+ end
+ end
+
+ def blob_file_name
+ @blob_file_name ||= params[:sha].sub('sha256:', '') + '.gz'
+ end
+
def group
strong_memoize(:group) do
- Group.find_by_full_path(params[:group_id], follow_redirects: request.get?)
+ Group.find_by_full_path(params[:group_id], follow_redirects: true)
end
end
diff --git a/app/controllers/groups/group_members_controller.rb b/app/controllers/groups/group_members_controller.rb
index 9b8d5cfe47..6e59f15963 100644
--- a/app/controllers/groups/group_members_controller.rb
+++ b/app/controllers/groups/group_members_controller.rb
@@ -25,19 +25,15 @@ class Groups::GroupMembersController < Groups::ApplicationController
def index
@sort = params[:sort].presence || sort_value_name
- @members = GroupMembersFinder
- .new(@group, current_user, params: filter_params)
- .execute(include_relations: requested_relations)
-
if can?(current_user, :admin_group_member, @group)
@skip_groups = @group.related_group_ids
- @invited_members = @members.invite
+ @invited_members = invited_members
@invited_members = @invited_members.search_invite_email(params[:search_invited]) if params[:search_invited].present?
@invited_members = present_invited_members(@invited_members)
end
- @members = present_group_members(@members.non_invite)
+ @members = present_group_members(non_invited_members)
@requesters = present_members(
AccessRequestsFinder.new(@group).execute(current_user)
@@ -51,6 +47,20 @@ class Groups::GroupMembersController < Groups::ApplicationController
private
+ def group_members
+ @group_members ||= GroupMembersFinder
+ .new(@group, current_user, params: filter_params)
+ .execute(include_relations: requested_relations)
+ end
+
+ def invited_members
+ group_members.invite.with_invited_user_state
+ end
+
+ def non_invited_members
+ group_members.non_invite
+ end
+
def present_invited_members(invited_members)
present_members(invited_members
.page(params[:invited_members_page])
diff --git a/app/controllers/groups/packages_controller.rb b/app/controllers/groups/packages_controller.rb
index 47f1816cc4..d02a826294 100644
--- a/app/controllers/groups/packages_controller.rb
+++ b/app/controllers/groups/packages_controller.rb
@@ -6,6 +6,10 @@ module Groups
feature_category :package_registry
+ before_action do
+ push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
+ end
+
private
def verify_packages_enabled!
diff --git a/app/controllers/groups/registry/repositories_controller.rb b/app/controllers/groups/registry/repositories_controller.rb
index 3aaaf6ade6..549a148bfb 100644
--- a/app/controllers/groups/registry/repositories_controller.rb
+++ b/app/controllers/groups/registry/repositories_controller.rb
@@ -3,6 +3,7 @@ module Groups
module Registry
class RepositoriesController < Groups::ApplicationController
include PackagesHelper
+ include ::Registry::ConnectionErrorsHandler
before_action :verify_container_registry_enabled!
before_action :authorize_read_container_image!
diff --git a/app/controllers/groups/runners_controller.rb b/app/controllers/groups/runners_controller.rb
index f37c08da22..5c21c7b023 100644
--- a/app/controllers/groups/runners_controller.rb
+++ b/app/controllers/groups/runners_controller.rb
@@ -10,8 +10,10 @@ class Groups::RunnersController < Groups::ApplicationController
feature_category :runner
def index
- finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
- @group_runners_limited_count = finder.execute.except(:limit, :offset).page.total_count_with_limit(:all, limit: 1000)
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') do
+ finder = Ci::RunnersFinder.new(current_user: current_user, params: { group: @group })
+ @group_runners_limited_count = finder.execute.except(:limit, :offset).page.total_count_with_limit(:all, limit: 1000)
+ end
end
def runner_list_group_view_vue_ui_enabled
@@ -61,9 +63,11 @@ class Groups::RunnersController < Groups::ApplicationController
private
def runner
- @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
- .except(:limit, :offset)
- .find(params[:id])
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') do
+ @runner ||= Ci::RunnersFinder.new(current_user: current_user, params: { group: @group }).execute
+ .except(:limit, :offset)
+ .find(params[:id])
+ end
end
def runner_params
diff --git a/app/controllers/groups/settings/ci_cd_controller.rb b/app/controllers/groups/settings/ci_cd_controller.rb
index a290ef9b5e..e125385f84 100644
--- a/app/controllers/groups/settings/ci_cd_controller.rb
+++ b/app/controllers/groups/settings/ci_cd_controller.rb
@@ -23,6 +23,11 @@ module Groups
@group_runners = runners_finder.execute.page(params[:page]).per(NUMBER_OF_RUNNERS_PER_PAGE)
@sort = runners_finder.sort_key
+
+ # Allow sql generated by the two relations above, @all_group_runners and @group_runners
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336433') do
+ render
+ end
end
def update
diff --git a/app/controllers/health_controller.rb b/app/controllers/health_controller.rb
index 99b0b77521..071378f266 100644
--- a/app/controllers/health_controller.rb
+++ b/app/controllers/health_controller.rb
@@ -16,6 +16,8 @@ class HealthController < ActionController::Base
Gitlab::HealthChecks::Redis::QueuesCheck,
Gitlab::HealthChecks::Redis::SharedStateCheck,
Gitlab::HealthChecks::Redis::TraceChunksCheck,
+ Gitlab::HealthChecks::Redis::RateLimitingCheck,
+ Gitlab::HealthChecks::Redis::SessionsCheck,
Gitlab::HealthChecks::GitalyCheck
].freeze
diff --git a/app/controllers/help_controller.rb b/app/controllers/help_controller.rb
index a1fb74cf27..0ad7478584 100644
--- a/app/controllers/help_controller.rb
+++ b/app/controllers/help_controller.rb
@@ -72,7 +72,6 @@ class HelpController < ApplicationController
end
def redirect_to_documentation_website?
- return false unless Feature.enabled?(:help_page_documentation_redirect)
return false unless Gitlab::UrlSanitizer.valid_web?(documentation_url)
true
diff --git a/app/controllers/import/bulk_imports_controller.rb b/app/controllers/import/bulk_imports_controller.rb
index da936215ad..bec26cb547 100644
--- a/app/controllers/import/bulk_imports_controller.rb
+++ b/app/controllers/import/bulk_imports_controller.rb
@@ -22,13 +22,16 @@ class Import::BulkImportsController < ApplicationController
def status
respond_to do |format|
format.json do
- data = importable_data
+ data = ::BulkImports::GetImportableDataService.new(params, query_params, credentials).execute
pagination_headers.each do |header|
- response.set_header(header, data.headers[header])
+ response.set_header(header, data[:response].headers[header])
end
- render json: { importable_data: serialized_data(data.parsed_response) }
+ json_response = { importable_data: serialized_data(data[:response].parsed_response) }
+ json_response[:version_validation] = data[:version_validation]
+
+ render json: json_response
end
format.html do
@source_url = session[url_key]
@@ -37,7 +40,7 @@ class Import::BulkImportsController < ApplicationController
end
def create
- response = BulkImportService.new(current_user, create_params, credentials).execute
+ response = ::BulkImports::CreateService.new(current_user, create_params, credentials).execute
if response.success?
render json: response.payload.to_json(only: [:id])
@@ -66,10 +69,6 @@ class Import::BulkImportsController < ApplicationController
@serializer ||= BaseSerializer.new(current_user: current_user)
end
- def importable_data
- client.get('groups', query_params)
- end
-
# Default query string params used to fetch groups from GitLab source instance
#
# top_level_only: fetch only top level groups (subgroups are fetched during import itself)
@@ -85,15 +84,6 @@ class Import::BulkImportsController < ApplicationController
query_params
end
- def client
- @client ||= BulkImports::Clients::HTTP.new(
- url: session[url_key],
- token: session[access_token_key],
- per_page: params[:per_page],
- page: params[:page]
- )
- end
-
def configure_params
params.permit(access_token_key, url_key)
end
diff --git a/app/controllers/import/url_controller.rb b/app/controllers/import/url_controller.rb
new file mode 100644
index 0000000000..4e4b6ad125
--- /dev/null
+++ b/app/controllers/import/url_controller.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+class Import::UrlController < ApplicationController
+ feature_category :importers
+
+ def validate
+ result = Import::ValidateRemoteGitEndpointService.new(validate_params).execute
+ if result.success?
+ render json: { success: true }
+ else
+ render json: { success: false, message: result.message }
+ end
+ end
+
+ private
+
+ def validate_params
+ params.permit(:user, :password, :url)
+ end
+end
diff --git a/app/controllers/jira_connect/app_descriptor_controller.rb b/app/controllers/jira_connect/app_descriptor_controller.rb
index 74fac6ff9b..e96242c705 100644
--- a/app/controllers/jira_connect/app_descriptor_controller.rb
+++ b/app/controllers/jira_connect/app_descriptor_controller.rb
@@ -32,6 +32,7 @@ class JiraConnect::AppDescriptorController < JiraConnect::ApplicationController
apiVersion: 1,
apiMigrations: {
'context-qsh': true,
+ 'signed-install': signed_install_active?,
gdpr: true
}
}
diff --git a/app/controllers/jira_connect/application_controller.rb b/app/controllers/jira_connect/application_controller.rb
index 352e78d625..ecb23c326f 100644
--- a/app/controllers/jira_connect/application_controller.rb
+++ b/app/controllers/jira_connect/application_controller.rb
@@ -74,4 +74,8 @@ class JiraConnect::ApplicationController < ApplicationController
params[:jwt] || request.headers['Authorization']&.split(' ', 2)&.last
end
end
+
+ def signed_install_active?
+ Feature.enabled?(:jira_connect_asymmetric_jwt)
+ end
end
diff --git a/app/controllers/jira_connect/events_controller.rb b/app/controllers/jira_connect/events_controller.rb
index fe66e742c4..76ac15f763 100644
--- a/app/controllers/jira_connect/events_controller.rb
+++ b/app/controllers/jira_connect/events_controller.rb
@@ -3,13 +3,18 @@
class JiraConnect::EventsController < JiraConnect::ApplicationController
# See https://developer.atlassian.com/cloud/jira/software/app-descriptor/#lifecycle
- skip_before_action :verify_atlassian_jwt!, only: :installed
- before_action :verify_qsh_claim!, only: :uninstalled
+ skip_before_action :verify_atlassian_jwt!
+ before_action :verify_asymmetric_atlassian_jwt!, if: :signed_install_active?
+
+ before_action :verify_atlassian_jwt!, only: :uninstalled, unless: :signed_install_active?
+ before_action :verify_qsh_claim!, only: :uninstalled, unless: :signed_install_active?
def installed
- return head :ok if atlassian_jwt_valid?
+ return head :ok if !signed_install_active? && atlassian_jwt_valid?
- installation = JiraConnectInstallation.new(install_params)
+ return head :ok if current_jira_installation
+
+ installation = JiraConnectInstallation.new(event_params)
if installation.save
head :ok
@@ -28,7 +33,23 @@ class JiraConnect::EventsController < JiraConnect::ApplicationController
private
- def install_params
+ def event_params
params.permit(:clientKey, :sharedSecret, :baseUrl).transform_keys(&:underscore)
end
+
+ def verify_asymmetric_atlassian_jwt!
+ asymmetric_jwt = Atlassian::JiraConnect::AsymmetricJwt.new(auth_token, jwt_verification_claims)
+
+ return head :unauthorized unless asymmetric_jwt.valid?
+
+ @current_jira_installation = JiraConnectInstallation.find_by_client_key(asymmetric_jwt.iss_claim)
+ end
+
+ def jwt_verification_claims
+ {
+ aud: jira_connect_base_url(protocol: 'https'),
+ iss: event_params[:client_key],
+ qsh: Atlassian::Jwt.create_query_string_hash(request.url, request.method, jira_connect_base_url)
+ }
+ end
end
diff --git a/app/controllers/metrics_controller.rb b/app/controllers/metrics_controller.rb
index a0c307a0a0..d3dea2ce15 100644
--- a/app/controllers/metrics_controller.rb
+++ b/app/controllers/metrics_controller.rb
@@ -7,6 +7,7 @@ class MetricsController < ActionController::Base
def index
response = if Gitlab::Metrics.prometheus_metrics_enabled?
+ Gitlab::Metrics::RailsSlis.initialize_request_slis_if_needed!
metrics_service.metrics_text
else
help_page = help_page_url('administration/monitoring/prometheus/gitlab_metrics',
diff --git a/app/controllers/profiles/passwords_controller.rb b/app/controllers/profiles/passwords_controller.rb
index c8c2dd1c7d..5eb0f80ddc 100644
--- a/app/controllers/profiles/passwords_controller.rb
+++ b/app/controllers/profiles/passwords_controller.rb
@@ -15,17 +15,11 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def create
- unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
+ unless @user.password_automatically_set || @user.valid_password?(user_params[:password])
redirect_to new_profile_password_path, alert: _('You must provide a valid current password')
return
end
- password_attributes = {
- password: user_params[:password],
- password_confirmation: user_params[:password_confirmation],
- password_automatically_set: false
- }
-
result = Users::UpdateService.new(current_user, password_attributes.merge(user: @user)).execute
if result[:status] == :success
@@ -41,12 +35,7 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def update
- password_attributes = user_params.select do |key, value|
- %w(password password_confirmation).include?(key.to_s)
- end
- password_attributes[:password_automatically_set] = false
-
- unless @user.password_automatically_set || @user.valid_password?(user_params[:current_password])
+ unless @user.password_automatically_set || @user.valid_password?(user_params[:password])
handle_invalid_current_password_attempt!
redirect_to edit_profile_password_path, alert: _('You must provide a valid current password')
@@ -94,6 +83,14 @@ class Profiles::PasswordsController < Profiles::ApplicationController
end
def user_params
- params.require(:user).permit(:current_password, :password, :password_confirmation)
+ params.require(:user).permit(:password, :new_password, :password_confirmation)
+ end
+
+ def password_attributes
+ {
+ password: user_params[:new_password],
+ password_confirmation: user_params[:password_confirmation],
+ password_automatically_set: false
+ }
end
end
diff --git a/app/controllers/profiles/two_factor_auths_controller.rb b/app/controllers/profiles/two_factor_auths_controller.rb
index de22a0e47d..e0b5d6be15 100644
--- a/app/controllers/profiles/two_factor_auths_controller.rb
+++ b/app/controllers/profiles/two_factor_auths_controller.rb
@@ -237,8 +237,6 @@ class Profiles::TwoFactorAuthsController < Profiles::ApplicationController
end
def ensure_verified_primary_email
- return unless Feature.enabled?(:ensure_verified_primary_email_for_2fa, default_enabled: :yaml)
-
unless current_user.two_factor_enabled? || current_user.primary_email_verified?
redirect_to profile_emails_path, notice: s_('You need to verify your primary email first before enabling Two-Factor Authentication.')
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index 29ae268ef6..69257081cc 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -18,7 +18,7 @@ class ProfilesController < Profiles::ApplicationController
def update
respond_to do |format|
- result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute
+ result = Users::UpdateService.new(current_user, user_params.merge(user: @user)).execute(check_password: true)
if result[:status] == :success
message = s_("Profiles|Profile was successfully updated")
@@ -129,6 +129,7 @@ class ProfilesController < Profiles::ApplicationController
:job_title,
:pronouns,
:pronunciation,
+ :validation_password,
status: [:emoji, :message, :availability]
]
end
diff --git a/app/controllers/projects/alerting/notifications_controller.rb b/app/controllers/projects/alerting/notifications_controller.rb
index db5d91308d..95b403faf5 100644
--- a/app/controllers/projects/alerting/notifications_controller.rb
+++ b/app/controllers/projects/alerting/notifications_controller.rb
@@ -3,6 +3,8 @@
module Projects
module Alerting
class NotificationsController < Projects::ApplicationController
+ include ActionController::HttpAuthentication::Basic
+
respond_to :json
skip_before_action :verify_authenticity_token
@@ -27,9 +29,19 @@ module Projects
end
def extract_alert_manager_token(request)
+ extract_bearer_token(request) || extract_basic_auth_token(request)
+ end
+
+ def extract_bearer_token(request)
Doorkeeper::OAuth::Token.from_bearer_authorization(request)
end
+ def extract_basic_auth_token(request)
+ _username, token = user_name_and_password(request)
+
+ token
+ end
+
def notify_service
notify_service_class.new(project, notification_payload)
end
diff --git a/app/controllers/projects/badges_controller.rb b/app/controllers/projects/badges_controller.rb
index f75ab5cdbf..0cd59c136e 100644
--- a/app/controllers/projects/badges_controller.rb
+++ b/app/controllers/projects/badges_controller.rb
@@ -24,7 +24,10 @@ class Projects::BadgesController < Projects::ApplicationController
.new(project, params[:ref], opts: {
job: params[:job],
key_text: params[:key_text],
- key_width: params[:key_width]
+ key_width: params[:key_width],
+ min_good: params[:min_good],
+ min_acceptable: params[:min_acceptable],
+ min_medium: params[:min_medium]
})
render_badge coverage_report
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index acf6b6116b..17fd28ee06 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -43,6 +43,7 @@ class Projects::BlobController < Projects::ApplicationController
before_action do
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:consolidated_edit_button, @project, default_enabled: :yaml)
push_licensed_feature(:file_locks) if @project.licensed_feature_available?(:file_locks)
end
diff --git a/app/controllers/projects/boards_controller.rb b/app/controllers/projects/boards_controller.rb
index 316582f399..834e4baa7d 100644
--- a/app/controllers/projects/boards_controller.rb
+++ b/app/controllers/projects/boards_controller.rb
@@ -11,6 +11,7 @@ class Projects::BoardsController < Projects::ApplicationController
push_frontend_feature_flag(:issue_boards_filtered_search, project, default_enabled: :yaml)
push_frontend_feature_flag(:board_multi_select, project, default_enabled: :yaml)
push_frontend_feature_flag(:iteration_cadences, project&.group, default_enabled: :yaml)
+ push_frontend_feature_flag(:labels_widget, project, default_enabled: :yaml)
end
feature_category :boards
diff --git a/app/controllers/projects/branches_controller.rb b/app/controllers/projects/branches_controller.rb
index 3be10559e8..b75effc52d 100644
--- a/app/controllers/projects/branches_controller.rb
+++ b/app/controllers/projects/branches_controller.rb
@@ -33,6 +33,11 @@ class Projects::BranchesController < Projects::ApplicationController
Gitlab::GitalyClient.allow_n_plus_1_calls do
render
end
+ rescue Gitlab::Git::CommandError => e
+ Gitlab::ErrorTracking.track_exception(e)
+
+ @gitaly_unavailable = true
+ render
end
format.json do
branches = BranchesFinder.new(@repository, params).execute
diff --git a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
index fee216da49..b2b5e09610 100644
--- a/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
+++ b/app/controllers/projects/ci/daily_build_group_report_results_controller.rb
@@ -4,7 +4,7 @@ class Projects::Ci::DailyBuildGroupReportResultsController < Projects::Applicati
before_action :authorize_read_build_report_results!
before_action :validate_param_type!
- feature_category :continuous_integration
+ feature_category :code_testing
def index
respond_to do |format|
diff --git a/app/controllers/projects/ci/pipeline_editor_controller.rb b/app/controllers/projects/ci/pipeline_editor_controller.rb
index 550877548e..22cd247644 100644
--- a/app/controllers/projects/ci/pipeline_editor_controller.rb
+++ b/app/controllers/projects/ci/pipeline_editor_controller.rb
@@ -3,8 +3,7 @@
class Projects::Ci::PipelineEditorController < Projects::ApplicationController
before_action :check_can_collaborate!
before_action do
- push_frontend_feature_flag(:pipeline_editor_empty_state_action, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:pipeline_editor_drawer, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:pipeline_editor_mini_graph, @project, default_enabled: :yaml)
push_frontend_feature_flag(:schema_linting, @project, default_enabled: :yaml)
end
diff --git a/app/controllers/projects/cluster_agents_controller.rb b/app/controllers/projects/cluster_agents_controller.rb
new file mode 100644
index 0000000000..e7fbe93131
--- /dev/null
+++ b/app/controllers/projects/cluster_agents_controller.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+class Projects::ClusterAgentsController < Projects::ApplicationController
+ before_action :authorize_can_read_cluster_agent!
+
+ feature_category :kubernetes_management
+
+ def show
+ @agent_name = params[:name]
+ end
+
+ private
+
+ def authorize_can_read_cluster_agent!
+ return if can?(current_user, :admin_cluster, project)
+
+ access_denied!
+ end
+end
diff --git a/app/controllers/projects/google_cloud_controller.rb b/app/controllers/projects/google_cloud_controller.rb
new file mode 100644
index 0000000000..d185457aeb
--- /dev/null
+++ b/app/controllers/projects/google_cloud_controller.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class Projects::GoogleCloudController < Projects::ApplicationController
+ before_action :authorize_can_manage_google_cloud_deployments!
+
+ feature_category :release_orchestration
+
+ def index
+ end
+
+ private
+
+ def authorize_can_manage_google_cloud_deployments!
+ access_denied! unless can?(current_user, :manage_project_google_cloud, project)
+ end
+end
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index f885ff9b45..fd508d5f12 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -37,7 +37,7 @@ class Projects::IssuesController < Projects::ApplicationController
before_action :authorize_download_code!, only: [:related_branches]
# Limit the amount of issues created per minute
- before_action :create_rate_limit, only: [:create]
+ before_action :create_rate_limit, only: [:create], if: -> { Feature.disabled?('rate_limited_service_issues_create', project, default_enabled: :yaml) }
before_action do
push_frontend_feature_flag(:tribute_autocomplete, @project)
diff --git a/app/controllers/projects/jobs_controller.rb b/app/controllers/projects/jobs_controller.rb
index 778623a05c..994be5c2b5 100644
--- a/app/controllers/projects/jobs_controller.rb
+++ b/app/controllers/projects/jobs_controller.rb
@@ -44,7 +44,7 @@ class Projects::JobsController < Projects::ApplicationController
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
- .represent(@build, {}, BuildDetailsEntity)
+ .represent(@build.present(current_user: current_user), {}, BuildDetailsEntity)
end
end
end
@@ -120,7 +120,7 @@ class Projects::JobsController < Projects::ApplicationController
def status
render json: BuildSerializer
.new(project: @project, current_user: @current_user)
- .represent_status(@build)
+ .represent_status(@build.present(current_user: current_user))
end
def erase
@@ -225,7 +225,6 @@ class Projects::JobsController < Projects::ApplicationController
def find_job_as_build
@build = project.builds.find(params[:id])
- .present(current_user: current_user)
end
def find_job_as_processable
diff --git a/app/controllers/projects/merge_requests_controller.rb b/app/controllers/projects/merge_requests_controller.rb
index cb68aaf458..46df514abc 100644
--- a/app/controllers/projects/merge_requests_controller.rb
+++ b/app/controllers/projects/merge_requests_controller.rb
@@ -37,10 +37,10 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
push_frontend_feature_flag(:core_security_mr_widget_counts, @project)
push_frontend_feature_flag(:paginated_notes, @project, default_enabled: :yaml)
push_frontend_feature_flag(:confidential_notes, @project, default_enabled: :yaml)
- push_frontend_feature_flag(:usage_data_i_testing_summary_widget_total, @project, default_enabled: :yaml)
push_frontend_feature_flag(:improved_emoji_picker, project, default_enabled: :yaml)
push_frontend_feature_flag(:diffs_virtual_scrolling, project, default_enabled: :yaml)
push_frontend_feature_flag(:restructured_mr_widget, project, default_enabled: :yaml)
+ push_frontend_feature_flag(:mr_changes_fluid_layout, project, default_enabled: :yaml)
# Usage data feature flags
push_frontend_feature_flag(:users_expanding_widgets_usage_data, @project, default_enabled: :yaml)
@@ -192,15 +192,17 @@ class Projects::MergeRequestsController < Projects::MergeRequests::ApplicationCo
Gitlab::PollingInterval.set_header(response, interval: 10_000)
- render json: {
- pipelines: PipelineSerializer
- .new(project: @project, current_user: @current_user)
- .with_pagination(request, response)
- .represent(@pipelines),
- count: {
- all: @pipelines.count
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336891') do
+ render json: {
+ pipelines: PipelineSerializer
+ .new(project: @project, current_user: @current_user)
+ .with_pagination(request, response)
+ .represent(@pipelines),
+ count: {
+ all: @pipelines.count
+ }
}
- }
+ end
end
def sast_reports
diff --git a/app/controllers/projects/packages/packages_controller.rb b/app/controllers/projects/packages/packages_controller.rb
index 5de71466c1..dd7c2ad3cb 100644
--- a/app/controllers/projects/packages/packages_controller.rb
+++ b/app/controllers/projects/packages/packages_controller.rb
@@ -7,6 +7,10 @@ module Projects
feature_category :package_registry
+ before_action do
+ push_frontend_feature_flag(:package_list_apollo, default_enabled: :yaml)
+ end
+
def show
@package = project.packages.find(params[:id])
end
diff --git a/app/controllers/projects/project_members_controller.rb b/app/controllers/projects/project_members_controller.rb
index b979276437..e8074f7d79 100644
--- a/app/controllers/projects/project_members_controller.rb
+++ b/app/controllers/projects/project_members_controller.rb
@@ -19,16 +19,12 @@ class Projects::ProjectMembersController < Projects::ApplicationController
@group_links = @project.project_group_links
@group_links = @group_links.search(params[:search_groups]) if params[:search_groups].present?
- project_members = MembersFinder
- .new(@project, current_user, params: filter_params)
- .execute(include_relations: requested_relations)
-
if can?(current_user, :admin_project_member, @project)
- @invited_members = present_members(project_members.invite)
+ @invited_members = present_members(invited_members)
@requesters = present_members(AccessRequestsFinder.new(@project).execute(current_user))
end
- @project_members = present_members(project_members.non_invite.page(params[:page]))
+ @project_members = present_members(non_invited_members.page(params[:page]))
@project_member = @project.project_members.new
end
@@ -55,6 +51,20 @@ class Projects::ProjectMembersController < Projects::ApplicationController
private
+ def members
+ @members ||= MembersFinder
+ .new(@project, current_user, params: filter_params)
+ .execute(include_relations: requested_relations)
+ end
+
+ def invited_members
+ members.invite.with_invited_user_state
+ end
+
+ def non_invited_members
+ members.non_invite
+ end
+
def filter_params
params.permit(:search).merge(sort: @sort)
end
diff --git a/app/controllers/projects/registry/repositories_controller.rb b/app/controllers/projects/registry/repositories_controller.rb
index 8acebd8903..ad3b2bc98e 100644
--- a/app/controllers/projects/registry/repositories_controller.rb
+++ b/app/controllers/projects/registry/repositories_controller.rb
@@ -4,6 +4,7 @@ module Projects
module Registry
class RepositoriesController < ::Projects::Registry::ApplicationController
include PackagesHelper
+ include ::Registry::ConnectionErrorsHandler
before_action :authorize_update_container_image!, only: [:destroy]
@@ -48,8 +49,6 @@ module Projects
repository.save! if repository.has_tags?
end
end
- rescue ContainerRegistry::Path::InvalidRegistryPathError
- @character_error = true
end
end
end
diff --git a/app/controllers/projects/security/configuration_controller.rb b/app/controllers/projects/security/configuration_controller.rb
index 19de157357..444f4783a1 100644
--- a/app/controllers/projects/security/configuration_controller.rb
+++ b/app/controllers/projects/security/configuration_controller.rb
@@ -5,7 +5,7 @@ module Projects
class ConfigurationController < Projects::ApplicationController
include SecurityAndCompliancePermissions
- feature_category :static_application_security_testing
+ feature_category :static_application_security_testing, [:show]
def show
render_403 unless can?(current_user, :read_security_configuration, project)
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 4168880001..3fc379a135 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -5,7 +5,7 @@ module Projects
class FunctionsController < Projects::ApplicationController
before_action :authorize_read_cluster!
- feature_category :serverless
+ feature_category :not_owned
def index
respond_to do |format|
diff --git a/app/controllers/projects/settings/ci_cd_controller.rb b/app/controllers/projects/settings/ci_cd_controller.rb
index 960c0beb24..3033dac824 100644
--- a/app/controllers/projects/settings/ci_cd_controller.rb
+++ b/app/controllers/projects/settings/ci_cd_controller.rb
@@ -25,6 +25,11 @@ module Projects
@project.triggers, current_user: current_user, project: @project
).to_json
end
+
+ # @assignable_runners is using ci_owned_runners
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/336436') do
+ render
+ end
end
def update
diff --git a/app/controllers/projects/tags_controller.rb b/app/controllers/projects/tags_controller.rb
index 94b0473e1f..02d36c3353 100644
--- a/app/controllers/projects/tags_controller.rb
+++ b/app/controllers/projects/tags_controller.rb
@@ -18,17 +18,21 @@ class Projects::TagsController < Projects::ApplicationController
params[:sort] = params[:sort].presence || sort_value_recently_updated
@sort = params[:sort]
- @tags = TagsFinder.new(@repository, params).execute
- @tags = Kaminari.paginate_array(@tags).page(params[:page])
+ @tags, @tags_loading_error = TagsFinder.new(@repository, params).execute
+
+ @tags = Kaminari.paginate_array(@tags).page(params[:page])
tag_names = @tags.map(&:name)
@tags_pipelines = @project.ci_pipelines.latest_successful_for_refs(tag_names)
+
@releases = project.releases.where(tag: tag_names)
@tag_pipeline_statuses = Ci::CommitStatusesFinder.new(@project, @repository, current_user, @tags).execute
respond_to do |format|
- format.html
- format.atom { render layout: 'xml.atom' }
+ status = @tags_loading_error ? :service_unavailable : :ok
+
+ format.html { render status: status }
+ format.atom { render layout: 'xml.atom', status: status }
end
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/controllers/projects/tree_controller.rb b/app/controllers/projects/tree_controller.rb
index 6fd4c632dd..a76d45411d 100644
--- a/app/controllers/projects/tree_controller.rb
+++ b/app/controllers/projects/tree_controller.rb
@@ -16,7 +16,9 @@ class Projects::TreeController < Projects::ApplicationController
before_action :authorize_edit_tree!, only: [:create_dir]
before_action do
+ push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
end
feature_category :source_code_management
diff --git a/app/controllers/projects/usage_quotas_controller.rb b/app/controllers/projects/usage_quotas_controller.rb
index 179c7fc8db..103e1cc596 100644
--- a/app/controllers/projects/usage_quotas_controller.rb
+++ b/app/controllers/projects/usage_quotas_controller.rb
@@ -9,6 +9,7 @@ class Projects::UsageQuotasController < Projects::ApplicationController
feature_category :utilization
def index
+ @hide_search_settings = true
@storage_app_data = {
project_path: @project.full_path,
usage_quotas_help_page_path: help_page_path('user/usage_quotas'),
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index d6bf0e340d..0760f97d7c 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -33,9 +33,12 @@ class ProjectsController < Projects::ApplicationController
before_action :export_rate_limit, only: [:export, :download_export, :generate_new_export]
before_action do
+ push_frontend_feature_flag(:lazy_load_commits, @project, default_enabled: :yaml)
push_frontend_feature_flag(:refactor_blob_viewer, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:refactor_text_viewer, @project, default_enabled: :yaml)
push_frontend_feature_flag(:increase_page_size_exponentially, @project, default_enabled: :yaml)
push_frontend_feature_flag(:paginated_tree_graphql_query, @project, default_enabled: :yaml)
+ push_frontend_feature_flag(:new_dir_modal, @project, default_enabled: :yaml)
end
layout :determine_layout
@@ -72,6 +75,13 @@ class ProjectsController < Projects::ApplicationController
@project = ::Projects::CreateService.new(current_user, project_params(attributes: project_params_create_attributes)).execute
if @project.saved?
+ experiment(:new_project_sast_enabled, user: current_user).track(:created,
+ property: active_new_project_tab,
+ checked: Gitlab::Utils.to_boolean(project_params[:initialize_with_sast]),
+ project: @project,
+ namespace: @project.namespace
+ )
+
redirect_to(
project_path(@project, custom_import_params),
notice: _("Project '%{project_name}' was successfully created.") % { project_name: @project.name }
@@ -283,9 +293,9 @@ class ProjectsController < Projects::ApplicationController
end
if find_tags && @repository.tag_count.nonzero?
- tags = TagsFinder.new(@repository, params).execute.take(100).map(&:name)
+ tags, _ = TagsFinder.new(@repository, params).execute
- options['Tags'] = tags
+ options['Tags'] = tags.take(100).map(&:name)
end
# If reference is commit id - we should add it to branch/tag selectbox
@@ -436,6 +446,7 @@ class ProjectsController < Projects::ApplicationController
:template_name,
:template_project_id,
:merge_method,
+ :initialize_with_sast,
:initialize_with_readme,
:autoclose_referenced_issues,
:suggestion_commit_message,
diff --git a/app/controllers/registrations_controller.rb b/app/controllers/registrations_controller.rb
index fe800de5dd..450c12a233 100644
--- a/app/controllers/registrations_controller.rb
+++ b/app/controllers/registrations_controller.rb
@@ -5,6 +5,7 @@ class RegistrationsController < Devise::RegistrationsController
include AcceptsPendingInvitations
include RecaptchaHelper
include InvisibleCaptchaOnSignup
+ include OneTrustCSP
layout 'devise'
@@ -45,6 +46,11 @@ class RegistrationsController < Devise::RegistrationsController
end
def destroy
+ if current_user.required_terms_not_accepted?
+ redirect_to profile_account_path, status: :see_other, alert: s_('Profiles|You must accept the Terms of Service in order to perform this action.')
+ return
+ end
+
if destroy_confirmation_valid?
current_user.delete_async(deleted_by: current_user)
session.try(:destroy)
diff --git a/app/controllers/repositories/git_http_controller.rb b/app/controllers/repositories/git_http_controller.rb
index e51bfe6a37..c3c6a51239 100644
--- a/app/controllers/repositories/git_http_controller.rb
+++ b/app/controllers/repositories/git_http_controller.rb
@@ -11,6 +11,9 @@ module Repositories
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccessProject::CreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
+ rescue_from GRPC::Unavailable do |e|
+ render_503_with_exception(e, message: 'The git server, Gitaly, is not available at this time. Please contact your administrator.')
+ end
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
@@ -71,8 +74,8 @@ module Repositories
render plain: exception.message, status: :unprocessable_entity
end
- def render_503_with_exception(exception)
- render plain: exception.message, status: :service_unavailable
+ def render_503_with_exception(exception, message: nil)
+ render plain: message || exception.message, status: :service_unavailable
end
def update_fetch_statistics
diff --git a/app/controllers/search_controller.rb b/app/controllers/search_controller.rb
index 5f1b3750e4..0a18559fc8 100644
--- a/app/controllers/search_controller.rb
+++ b/app/controllers/search_controller.rb
@@ -12,6 +12,7 @@ class SearchController < ApplicationController
around_action :allow_gitaly_ref_name_caching
before_action :block_anonymous_global_searches, :check_scope_global_search_enabled, except: :opensearch
+ before_action :strip_surrounding_whitespace_from_search, except: :opensearch
skip_before_action :authenticate_user!
requires_cross_project_access if: -> do
search_term_present = params[:search].present? || params[:term].present?
@@ -23,6 +24,7 @@ class SearchController < ApplicationController
layout 'search'
feature_category :global_search
+ urgency :high, [:opensearch]
def show
@project = search_service.project
@@ -196,6 +198,10 @@ class SearchController < ApplicationController
def count_action_name?
action_name.to_sym == :count
end
+
+ def strip_surrounding_whitespace_from_search
+ %i(term search).each { |param| params[param]&.strip! }
+ end
end
SearchController.prepend_mod_with('SearchController')
diff --git a/app/controllers/sessions_controller.rb b/app/controllers/sessions_controller.rb
index 4fcf82c605..bbd7e5d572 100644
--- a/app/controllers/sessions_controller.rb
+++ b/app/controllers/sessions_controller.rb
@@ -9,6 +9,7 @@ class SessionsController < Devise::SessionsController
include RendersLdapServers
include KnownSignIn
include Gitlab::Utils::StrongMemoize
+ include OneTrustCSP
skip_before_action :check_two_factor_requirement, only: [:destroy]
skip_before_action :check_password_expiration, only: [:destroy]
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index d040ac7f76..d7eb3ccd27 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -13,6 +13,7 @@ class UploadsController < ApplicationController
"group" => Group,
"appearance" => Appearance,
"personal_snippet" => PersonalSnippet,
+ "projects/topic" => Projects::Topic,
nil => PersonalSnippet
}.freeze
@@ -54,6 +55,8 @@ class UploadsController < ApplicationController
!secret? || can?(current_user, :update_user, model)
when Appearance
true
+ when Projects::Topic
+ true
else
permission = "read_#{model.class.underscore}".to_sym
@@ -85,7 +88,7 @@ class UploadsController < ApplicationController
def cache_settings
case model
- when User, Appearance
+ when User, Appearance, Projects::Topic
[5.minutes, { public: true, must_revalidate: false }]
when Project, Group
[5.minutes, { private: true, must_revalidate: true }]
diff --git a/app/experiments/new_project_sast_enabled_experiment.rb b/app/experiments/new_project_sast_enabled_experiment.rb
new file mode 100644
index 0000000000..1ab86d7013
--- /dev/null
+++ b/app/experiments/new_project_sast_enabled_experiment.rb
@@ -0,0 +1,15 @@
+# frozen_string_literal: true
+
+class NewProjectSastEnabledExperiment < ApplicationExperiment # rubocop:disable Gitlab/NamespacedClass
+ def publish(_result = nil)
+ super
+
+ publish_to_database
+ end
+
+ def candidate_behavior
+ end
+
+ def free_indicator_behavior
+ end
+end
diff --git a/app/finders/ci/pipelines_for_merge_request_finder.rb b/app/finders/ci/pipelines_for_merge_request_finder.rb
index 5d794c0903..9476c30f52 100644
--- a/app/finders/ci/pipelines_for_merge_request_finder.rb
+++ b/app/finders/ci/pipelines_for_merge_request_finder.rb
@@ -5,6 +5,8 @@ module Ci
class PipelinesForMergeRequestFinder
include Gitlab::Utils::StrongMemoize
+ COMMITS_LIMIT = 100
+
def initialize(merge_request, current_user)
@merge_request = merge_request
@current_user = current_user
@@ -12,7 +14,7 @@ module Ci
attr_reader :merge_request, :current_user
- delegate :commit_shas, :target_project, :source_project, :source_branch, to: :merge_request
+ delegate :recent_diff_head_shas, :commit_shas, :target_project, :source_project, :source_branch, to: :merge_request
# Fetch all pipelines that the user can read.
def execute
@@ -35,7 +37,7 @@ module Ci
pipelines =
if merge_request.persisted?
- pipelines_using_cte
+ all_pipelines_for_merge_request
else
triggered_for_branch.for_sha(commit_shas)
end
@@ -79,6 +81,17 @@ module Ci
pipelines.joins(shas_table) # rubocop: disable CodeReuse/ActiveRecord
end
+ def all_pipelines_for_merge_request
+ if Feature.enabled?(:decomposed_ci_query_in_pipelines_for_merge_request_finder, target_project, default_enabled: :yaml)
+ pipelines_for_merge_request = triggered_by_merge_request
+ pipelines_for_branch = triggered_for_branch.for_sha(recent_diff_head_shas(COMMITS_LIMIT))
+
+ Ci::Pipeline.from_union([pipelines_for_merge_request, pipelines_for_branch])
+ else
+ pipelines_using_cte
+ end
+ end
+
# NOTE: this method returns only parent merge request pipelines.
# Child merge request pipelines have a different source.
def triggered_by_merge_request
diff --git a/app/finders/clusters/agents_finder.rb b/app/finders/clusters/agents_finder.rb
new file mode 100644
index 0000000000..136bbf1698
--- /dev/null
+++ b/app/finders/clusters/agents_finder.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentsFinder
+ def initialize(project, current_user, params: {})
+ @project = project
+ @current_user = current_user
+ @params = params
+ end
+
+ def execute
+ return ::Clusters::Agent.none unless can_read_cluster_agents?
+
+ agents = project.cluster_agents
+ agents = agents.with_name(params[:name]) if params[:name].present?
+
+ agents.ordered_by_name
+ end
+
+ private
+
+ attr_reader :project, :current_user, :params
+
+ def can_read_cluster_agents?
+ current_user.can?(:read_cluster, project)
+ end
+ end
+end
diff --git a/app/finders/concerns/packages/finder_helper.rb b/app/finders/concerns/packages/finder_helper.rb
index d2784a1d27..0ae99782cd 100644
--- a/app/finders/concerns/packages/finder_helper.rb
+++ b/app/finders/concerns/packages/finder_helper.rb
@@ -54,6 +54,12 @@ module Packages
packages.search_by_name(params[:package_name])
end
+ def filter_by_exact_package_name(packages)
+ return packages unless params[:package_name].present?
+
+ packages.with_name(params[:package_name])
+ end
+
def filter_by_package_version(packages)
return packages unless params[:package_version].present?
diff --git a/app/finders/error_tracking/errors_finder.rb b/app/finders/error_tracking/errors_finder.rb
index d83a0c487e..c361d6e2fc 100644
--- a/app/finders/error_tracking/errors_finder.rb
+++ b/app/finders/error_tracking/errors_finder.rb
@@ -15,8 +15,7 @@ module ErrorTracking
collection = by_status(collection)
collection = sort(collection)
- # Limit collection until pagination implemented.
- limit(collection)
+ collection.keyset_paginate(cursor: params[:cursor], per_page: limit)
end
private
@@ -39,9 +38,9 @@ module ErrorTracking
params[:sort] ? collection.sort_by_attribute(params[:sort]) : collection.order_id_desc
end
- def limit(collection)
+ def limit
# Restrict the maximum limit at 100 records.
- collection.limit([(params[:limit] || 20).to_i, 100].min)
+ [(params[:limit] || 20).to_i, 100].min
end
end
end
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index cf706a8f98..7b0cd17a76 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -194,8 +194,7 @@ class IssuableFinder
def use_cte_for_search?
strong_memoize(:use_cte_for_search) do
next false unless search
- # Only simple unsorted & simple sorts can use CTE
- next false if params[:sort].present? && !params[:sort].in?(klass.simple_sorts.keys)
+ next false unless default_or_simple_sort?
attempt_group_search_optimizations? || attempt_project_search_optimizations?
end
@@ -244,6 +243,10 @@ class IssuableFinder
klass.all
end
+ def default_or_simple_sort?
+ params[:sort].blank? || params[:sort].to_s.in?(klass.simple_sorts.keys)
+ end
+
def attempt_group_search_optimizations?
params[:attempt_group_search_optimizations]
end
diff --git a/app/finders/issuables/label_filter.rb b/app/finders/issuables/label_filter.rb
index 2bbc963aa9..f4712fa687 100644
--- a/app/finders/issuables/label_filter.rb
+++ b/app/finders/issuables/label_filter.rb
@@ -89,17 +89,25 @@ module Issuables
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def find_label_ids(label_names)
- group_labels = Label
- .where(project_id: nil)
- .where(title: label_names)
- .where(group_id: root_namespace.self_and_descendant_ids)
+ find_label_ids_uncached(label_names)
+ end
+ # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
+ request_cache(:find_label_ids) { root_namespace.id }
- project_labels = Label
- .where(group_id: nil)
- .where(title: label_names)
- .where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendant_ids))
+ # This returns an array of label IDs per label name. It is possible for a label name
+ # to have multiple IDs because we allow labels with the same name if they are on a different
+ # project or group.
+ #
+ # For example, if we pass in `['bug', 'feature']`, this will return something like:
+ # `[ [1, 2], [3] ]`
+ #
+ # rubocop: disable CodeReuse/ActiveRecord
+ def find_label_ids_uncached(label_names)
+ return [] if label_names.empty?
+
+ group_labels = group_labels_for_root_namespace.where(title: label_names)
+ project_labels = project_labels_for_root_namespace.where(title: label_names)
Label
.from_union([group_labels, project_labels], remove_duplicates: false)
@@ -109,8 +117,18 @@ module Issuables
.values
.map { |labels| labels.map(&:last) }
end
- # Avoid repeating label queries times when the finder is instantiated multiple times during the request.
- request_cache(:find_label_ids) { root_namespace.id }
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def group_labels_for_root_namespace
+ Label.where(project_id: nil).where(group_id: root_namespace.self_and_descendant_ids)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def project_labels_for_root_namespace
+ Label.where(group_id: nil).where(project_id: Project.select(:id).where(namespace_id: root_namespace.self_and_descendant_ids))
+ end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
@@ -153,3 +171,5 @@ module Issuables
end
end
end
+
+Issuables::LabelFilter.prepend_mod
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index abf0c180d6..21a19aa22a 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -91,6 +91,12 @@ class IssuesFinder < IssuableFinder
by_issue_types(issues)
end
+ # Negates all params found in `negatable_params`
+ def filter_negated_items(items)
+ issues = super
+ by_negated_issue_types(issues)
+ end
+
def by_confidential(items)
return items if params[:confidential].nil?
@@ -122,6 +128,13 @@ class IssuesFinder < IssuableFinder
items.with_issue_type(params[:issue_types])
end
+
+ def by_negated_issue_types(items)
+ issue_type_params = Array(not_params[:issue_types]).map(&:to_s) & WorkItem::Type.base_types.keys
+ return items if issue_type_params.blank?
+
+ items.without_issue_type(issue_type_params)
+ end
end
IssuesFinder.prepend_mod_with('IssuesFinder')
diff --git a/app/finders/members_finder.rb b/app/finders/members_finder.rb
index ea101cf1dc..0faafa6df9 100644
--- a/app/finders/members_finder.rb
+++ b/app/finders/members_finder.rb
@@ -70,11 +70,16 @@ class MembersFinder
end
def project_invited_groups
- invited_groups_ids_including_ancestors = Gitlab::ObjectHierarchy
- .new(project.invited_groups)
- .base_and_ancestors
- .public_or_visible_to_user(current_user)
- .select(:id)
+ invited_groups_and_ancestors = if ::Feature.enabled?(:linear_members_finder_ancestor_scopes, current_user, default_enabled: :yaml)
+ project.invited_groups
+ .self_and_ancestors
+ else
+ Gitlab::ObjectHierarchy
+ .new(project.invited_groups)
+ .base_and_ancestors
+ end
+
+ invited_groups_ids_including_ancestors = invited_groups_and_ancestors.public_or_visible_to_user(current_user).select(:id)
GroupMember.with_source_id(invited_groups_ids_including_ancestors).non_minimal_access
end
diff --git a/app/finders/packages/group_packages_finder.rb b/app/finders/packages/group_packages_finder.rb
index e753fa4d45..3ac5f00d51 100644
--- a/app/finders/packages/group_packages_finder.rb
+++ b/app/finders/packages/group_packages_finder.rb
@@ -4,7 +4,7 @@ module Packages
class GroupPackagesFinder
include ::Packages::FinderHelper
- def initialize(current_user, group, params = { exclude_subgroups: false, order_by: 'created_at', sort: 'asc' })
+ def initialize(current_user, group, params = { exclude_subgroups: false, exact_name: false, order_by: 'created_at', sort: 'asc' })
@current_user = current_user
@group = group
@params = params
@@ -30,7 +30,7 @@ module Packages
packages = filter_with_version(packages)
packages = filter_by_package_type(packages)
- packages = filter_by_package_name(packages)
+ packages = (params[:exact_name] ? filter_by_exact_package_name(packages) : filter_by_package_name(packages))
packages = filter_by_package_version(packages)
installable_only ? packages.installable : filter_by_status(packages)
end
diff --git a/app/finders/projects/members/effective_access_level_finder.rb b/app/finders/projects/members/effective_access_level_finder.rb
index c1e3842a9e..d238679f2f 100644
--- a/app/finders/projects/members/effective_access_level_finder.rb
+++ b/app/finders/projects/members/effective_access_level_finder.rb
@@ -99,7 +99,7 @@ module Projects
end
def include_membership_from_project_group_shares?
- project.allowed_to_share_with_group? && project.project_group_links.any?
+ !project.namespace.share_with_group_lock && project.project_group_links.any?
end
# methods for `select` options
diff --git a/app/finders/projects/topics_finder.rb b/app/finders/projects/topics_finder.rb
new file mode 100644
index 0000000000..7c3abc27cf
--- /dev/null
+++ b/app/finders/projects/topics_finder.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+# Used to filter project topics by a set of params
+#
+# Arguments:
+# params:
+# search: string
+module Projects
+ class TopicsFinder
+ def initialize(params: {})
+ @params = params
+ end
+
+ def execute
+ topics = Projects::Topic.order_by_total_projects_count
+ by_search(topics)
+ end
+
+ private
+
+ attr_reader :current_user, :params
+
+ def by_search(topics)
+ return topics unless params[:search].present?
+
+ topics.search(params[:search]).reorder_by_similarity(params[:search])
+ end
+ end
+end
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 5537058cc7..7245bb36ac 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -182,8 +182,8 @@ class ProjectsFinder < UnionFinder
def by_topics(items)
return items unless params[:topic].present?
- topics = params[:topic].instance_of?(String) ? params[:topic].strip.split(/\s*,\s*/) : params[:topic]
- topics.each do |topic|
+ topics = params[:topic].instance_of?(String) ? params[:topic].split(',') : params[:topic]
+ topics.map(&:strip).uniq.reject(&:empty?).each do |topic|
items = items.with_topic(topic)
end
diff --git a/app/finders/tags_finder.rb b/app/finders/tags_finder.rb
index d9848d027c..0ccbbdc1b8 100644
--- a/app/finders/tags_finder.rb
+++ b/app/finders/tags_finder.rb
@@ -7,6 +7,9 @@ class TagsFinder < GitRefsFinder
def execute
tags = repository.tags_sorted_by(sort)
- by_search(tags)
+
+ [by_search(tags), nil]
+ rescue Gitlab::Git::CommandError => e
+ [[], e]
end
end
diff --git a/app/graphql/mutations/ci/runner/delete.rb b/app/graphql/mutations/ci/runner/delete.rb
index 8d9a5f1550..88dc426398 100644
--- a/app/graphql/mutations/ci/runner/delete.rb
+++ b/app/graphql/mutations/ci/runner/delete.rb
@@ -28,7 +28,7 @@ module Mutations
def authenticate_delete_runner!(runner)
return if current_user.can_admin_all_resources?
- "Runner #{runner.to_global_id} associated with more than one project" if runner.projects.count > 1
+ "Runner #{runner.to_global_id} associated with more than one project" if runner.runner_projects.count > 1
end
def find_object(id)
diff --git a/app/graphql/mutations/clusters/agent_tokens/create.rb b/app/graphql/mutations/clusters/agent_tokens/create.rb
new file mode 100644
index 0000000000..07bf253606
--- /dev/null
+++ b/app/graphql/mutations/clusters/agent_tokens/create.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module AgentTokens
+ class Create < BaseMutation
+ graphql_name 'ClusterAgentTokenCreate'
+
+ authorize :create_cluster
+
+ ClusterAgentID = ::Types::GlobalIDType[::Clusters::Agent]
+
+ argument :cluster_agent_id,
+ ClusterAgentID,
+ required: true,
+ description: 'Global ID of the cluster agent that will be associated with the new token.'
+
+ argument :description,
+ GraphQL::Types::String,
+ required: false,
+ description: 'Description of the token.'
+
+ argument :name,
+ GraphQL::Types::String,
+ required: true,
+ description: 'Name of the token.'
+
+ field :secret,
+ GraphQL::Types::String,
+ null: true,
+ description: "Token secret value. Make sure you save it - you won't be able to access it again."
+
+ field :token,
+ Types::Clusters::AgentTokenType,
+ null: true,
+ description: 'Token created after mutation.'
+
+ def resolve(args)
+ cluster_agent = authorized_find!(id: args[:cluster_agent_id])
+
+ result = ::Clusters::AgentTokens::CreateService
+ .new(
+ container: cluster_agent.project,
+ current_user: current_user,
+ params: args.merge(agent_id: cluster_agent.id)
+ )
+ .execute
+
+ payload = result.payload
+
+ {
+ secret: payload[:secret],
+ token: payload[:token],
+ errors: Array.wrap(result.message)
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = ClusterAgentID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agent_tokens/delete.rb b/app/graphql/mutations/clusters/agent_tokens/delete.rb
new file mode 100644
index 0000000000..603b6b3091
--- /dev/null
+++ b/app/graphql/mutations/clusters/agent_tokens/delete.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module AgentTokens
+ class Delete < BaseMutation
+ graphql_name 'ClusterAgentTokenDelete'
+
+ authorize :admin_cluster
+
+ TokenID = ::Types::GlobalIDType[::Clusters::AgentToken]
+
+ argument :id, TokenID,
+ required: true,
+ description: 'Global ID of the cluster agent token that will be deleted.'
+
+ def resolve(id:)
+ token = authorized_find!(id: id)
+ token.destroy
+
+ { errors: errors_on_object(token) }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = TokenID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agents/create.rb b/app/graphql/mutations/clusters/agents/create.rb
new file mode 100644
index 0000000000..0896cc7b20
--- /dev/null
+++ b/app/graphql/mutations/clusters/agents/create.rb
@@ -0,0 +1,38 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module Agents
+ class Create < BaseMutation
+ include FindsProject
+
+ authorize :create_cluster
+
+ graphql_name 'CreateClusterAgent'
+
+ argument :project_path, GraphQL::Types::ID,
+ required: true,
+ description: 'Full path of the associated project for this cluster agent.'
+
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Name of the cluster agent.'
+
+ field :cluster_agent,
+ Types::Clusters::AgentType,
+ null: true,
+ description: 'Cluster agent created after mutation.'
+
+ def resolve(project_path:, name:)
+ project = authorized_find!(project_path)
+ result = ::Clusters::Agents::CreateService.new(project, current_user).execute(name: name)
+
+ {
+ cluster_agent: result[:cluster_agent],
+ errors: Array.wrap(result[:message])
+ }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/clusters/agents/delete.rb b/app/graphql/mutations/clusters/agents/delete.rb
new file mode 100644
index 0000000000..9ada1f31f6
--- /dev/null
+++ b/app/graphql/mutations/clusters/agents/delete.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+module Mutations
+ module Clusters
+ module Agents
+ class Delete < BaseMutation
+ graphql_name 'ClusterAgentDelete'
+
+ authorize :admin_cluster
+
+ AgentID = ::Types::GlobalIDType[::Clusters::Agent]
+
+ argument :id, AgentID,
+ required: true,
+ description: 'Global ID of the cluster agent that will be deleted.'
+
+ def resolve(id:)
+ cluster_agent = authorized_find!(id: id)
+ result = ::Clusters::Agents::DeleteService
+ .new(container: cluster_agent.project, current_user: current_user)
+ .execute(cluster_agent)
+
+ {
+ errors: Array.wrap(result.message)
+ }
+ end
+
+ private
+
+ def find_object(id:)
+ # TODO: remove this line when the compatibility layer is removed
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
+ id = AgentID.coerce_isolated_input(id)
+ GitlabSchema.find_by_gid(id)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/customer_relations/contacts/create.rb b/app/graphql/mutations/customer_relations/contacts/create.rb
new file mode 100644
index 0000000000..77b4864468
--- /dev/null
+++ b/app/graphql/mutations/customer_relations/contacts/create.rb
@@ -0,0 +1,69 @@
+# frozen_string_literal: true
+
+module Mutations
+ module CustomerRelations
+ module Contacts
+ class Create < BaseMutation
+ include ResolvesIds
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+
+ graphql_name 'CustomerRelationsContactCreate'
+
+ field :contact,
+ Types::CustomerRelations::ContactType,
+ null: true,
+ description: 'Contact after the mutation.'
+
+ argument :group_id, ::Types::GlobalIDType[::Group],
+ required: true,
+ description: 'Group for the contact.'
+
+ argument :organization_id, ::Types::GlobalIDType[::CustomerRelations::Organization],
+ required: false,
+ description: 'Organization for the contact.'
+
+ argument :first_name, GraphQL::Types::String,
+ required: true,
+ description: 'First name of the contact.'
+
+ argument :last_name, GraphQL::Types::String,
+ required: true,
+ description: 'Last name of the contact.'
+
+ argument :phone, GraphQL::Types::String,
+ required: false,
+ description: 'Phone number of the contact.'
+
+ argument :email, GraphQL::Types::String,
+ required: false,
+ description: 'Email address of the contact.'
+
+ argument :description, GraphQL::Types::String,
+ required: false,
+ description: 'Description of or notes for the contact.'
+
+ authorize :admin_contact
+
+ def resolve(args)
+ group = authorized_find!(id: args[:group_id])
+
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
+
+ set_organization!(args)
+ result = ::CustomerRelations::Contacts::CreateService.new(group: group, current_user: current_user, params: args).execute
+ { contact: result.payload, errors: result.errors }
+ end
+
+ def find_object(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::Group)
+ end
+
+ def set_organization!(args)
+ return unless args[:organization_id]
+
+ args[:organization_id] = resolve_ids(args[:organization_id], ::Types::GlobalIDType[::CustomerRelations::Organization])[0]
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/customer_relations/contacts/update.rb b/app/graphql/mutations/customer_relations/contacts/update.rb
new file mode 100644
index 0000000000..e9e7c9b6ab
--- /dev/null
+++ b/app/graphql/mutations/customer_relations/contacts/update.rb
@@ -0,0 +1,61 @@
+# frozen_string_literal: true
+
+module Mutations
+ module CustomerRelations
+ module Contacts
+ class Update < Mutations::BaseMutation
+ include ResolvesIds
+
+ graphql_name 'CustomerRelationsContactUpdate'
+
+ authorize :admin_contact
+
+ field :contact,
+ Types::CustomerRelations::ContactType,
+ null: true,
+ description: 'Contact after the mutation.'
+
+ argument :id, ::Types::GlobalIDType[::CustomerRelations::Contact],
+ required: true,
+ description: 'Global ID of the contact.'
+
+ argument :organization_id, ::Types::GlobalIDType[::CustomerRelations::Organization],
+ required: false,
+ description: 'Organization of the contact.'
+
+ argument :first_name, GraphQL::Types::String,
+ required: false,
+ description: 'First name of the contact.'
+
+ argument :last_name, GraphQL::Types::String,
+ required: false,
+ description: 'Last name of the contact.'
+
+ argument :phone, GraphQL::Types::String,
+ required: false,
+ description: 'Phone number of the contact.'
+
+ argument :email, GraphQL::Types::String,
+ required: false,
+ description: 'Email address of the contact.'
+
+ argument :description, GraphQL::Types::String,
+ required: false,
+ description: 'Description of or notes for the contact.'
+
+ def resolve(args)
+ contact = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Contact))
+ raise_resource_not_available_error! unless contact
+
+ group = contact.group
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless Feature.enabled?(:customer_relations, group, default_enabled: :yaml)
+
+ authorize!(group)
+
+ result = ::CustomerRelations::Contacts::UpdateService.new(group: group, current_user: current_user, params: args).execute(contact)
+ { contact: result.payload, errors: result.errors }
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/customer_relations/organizations/create.rb b/app/graphql/mutations/customer_relations/organizations/create.rb
index 3fa7b0327c..bb02e1f734 100644
--- a/app/graphql/mutations/customer_relations/organizations/create.rb
+++ b/app/graphql/mutations/customer_relations/organizations/create.rb
@@ -31,7 +31,7 @@ module Mutations
argument :description,
GraphQL::Types::String,
required: false,
- description: 'Description or notes for the organization.'
+ description: 'Description of or notes for the organization.'
authorize :admin_organization
diff --git a/app/graphql/mutations/customer_relations/organizations/update.rb b/app/graphql/mutations/customer_relations/organizations/update.rb
index c6ae62193f..d8eb55d77e 100644
--- a/app/graphql/mutations/customer_relations/organizations/update.rb
+++ b/app/graphql/mutations/customer_relations/organizations/update.rb
@@ -32,7 +32,7 @@ module Mutations
argument :description,
GraphQL::Types::String,
required: false,
- description: 'Description or notes for the organization.'
+ description: 'Description of or notes for the organization.'
def resolve(args)
organization = ::Gitlab::Graphql::Lazy.force(GitlabSchema.object_from_id(args.delete(:id), expected_type: ::CustomerRelations::Organization))
diff --git a/app/graphql/mutations/dependency_proxy/group_settings/update.rb b/app/graphql/mutations/dependency_proxy/group_settings/update.rb
new file mode 100644
index 0000000000..d10e43cde2
--- /dev/null
+++ b/app/graphql/mutations/dependency_proxy/group_settings/update.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module Mutations
+ module DependencyProxy
+ module GroupSettings
+ class Update < Mutations::BaseMutation
+ include Mutations::ResolvesGroup
+
+ graphql_name 'UpdateDependencyProxySettings'
+
+ authorize :admin_dependency_proxy
+
+ argument :group_path,
+ GraphQL::Types::ID,
+ required: true,
+ description: 'Group path for the group dependency proxy.'
+
+ argument :enabled,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: copy_field_description(Types::DependencyProxy::ImageTtlGroupPolicyType, :enabled)
+
+ field :dependency_proxy_setting,
+ Types::DependencyProxy::GroupSettingType,
+ null: true,
+ description: 'Group dependency proxy settings after mutation.'
+
+ def resolve(group_path:, **args)
+ group = authorized_find!(group_path: group_path)
+
+ result = ::DependencyProxy::GroupSettings::UpdateService
+ .new(container: group, current_user: current_user, params: args)
+ .execute
+
+ {
+ dependency_proxy_setting: result.payload[:dependency_proxy_setting],
+ errors: result.errors
+ }
+ end
+
+ private
+
+ def find_object(group_path:)
+ resolve_group(full_path: group_path)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/mutations/issues/create.rb b/app/graphql/mutations/issues/create.rb
index 32f96f1bfe..70a8f539cc 100644
--- a/app/graphql/mutations/issues/create.rb
+++ b/app/graphql/mutations/issues/create.rb
@@ -71,7 +71,7 @@ module Mutations
def resolve(project_path:, **attributes)
project = authorized_find!(project_path)
- params = build_create_issue_params(attributes.merge(author_id: current_user.id))
+ params = build_create_issue_params(attributes.merge(author_id: current_user.id), project)
spam_params = ::Spam::SpamParams.new_from_request(request: context[:request])
issue = ::Issues::CreateService.new(project: project, current_user: current_user, params: params, spam_params: spam_params).execute
@@ -88,7 +88,8 @@ module Mutations
private
- def build_create_issue_params(params)
+ # _project argument is unused here, but it is necessary on the EE version of the method
+ def build_create_issue_params(params, _project)
params[:milestone_id] &&= params[:milestone_id]&.model_id
params[:assignee_ids] &&= params[:assignee_ids].map { |assignee_id| assignee_id&.model_id }
params[:label_ids] &&= params[:label_ids].map { |label_id| label_id&.model_id }
diff --git a/app/graphql/resolvers/board_list_issues_resolver.rb b/app/graphql/resolvers/board_list_issues_resolver.rb
index 7c85dd8fb9..d70acdf7ca 100644
--- a/app/graphql/resolvers/board_list_issues_resolver.rb
+++ b/app/graphql/resolvers/board_list_issues_resolver.rb
@@ -18,11 +18,8 @@ module Resolvers
filter_params = filters.merge(board_id: list.board.id, id: list.id)
service = ::Boards::Issues::ListService.new(list.board.resource_parent, context[:current_user], filter_params)
- pagination_connections = Gitlab::Graphql::Pagination::Keyset::Connection.new(service.execute)
- ::Boards::Issues::ListService.initialize_relative_positions(list.board, current_user, pagination_connections.items)
-
- pagination_connections
+ service.execute
end
# https://gitlab.com/gitlab-org/gitlab/-/issues/235681
diff --git a/app/graphql/resolvers/board_list_resolver.rb b/app/graphql/resolvers/board_list_resolver.rb
new file mode 100644
index 0000000000..d853846b67
--- /dev/null
+++ b/app/graphql/resolvers/board_list_resolver.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Resolvers
+ class BoardListResolver < BaseResolver.single
+ include Gitlab::Graphql::Authorize::AuthorizeResource
+ include BoardItemFilterable
+
+ type Types::BoardListType, null: true
+ description 'Find an issue board list.'
+
+ authorize :read_issue_board_list
+
+ argument :id, Types::GlobalIDType[List],
+ required: true,
+ description: 'Global ID of the list.'
+
+ argument :issue_filters, Types::Boards::BoardIssueInputType,
+ required: false,
+ description: 'Filters applied when getting issue metadata in the board list.'
+
+ def resolve(id: nil, issue_filters: {})
+ context.scoped_set!(:issue_filters, item_filters(issue_filters))
+
+ Gitlab::Graphql::Lazy.with_value(find_list(id: id)) do |list|
+ list if authorized_resource?(list)
+ end
+ end
+
+ private
+
+ def find_list(id:)
+ GitlabSchema.object_from_id(id, expected_type: ::List)
+ end
+ end
+end
diff --git a/app/graphql/resolvers/clusters/agent_tokens_resolver.rb b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
new file mode 100644
index 0000000000..5ae19700fd
--- /dev/null
+++ b/app/graphql/resolvers/clusters/agent_tokens_resolver.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Clusters
+ class AgentTokensResolver < BaseResolver
+ type Types::Clusters::AgentTokenType, null: true
+
+ alias_method :agent, :object
+
+ delegate :project, to: :agent
+
+ def resolve(**args)
+ return ::Clusters::AgentToken.none unless can_read_agent_tokens?
+
+ agent.last_used_agent_tokens
+ end
+
+ private
+
+ def can_read_agent_tokens?
+ current_user.can?(:admin_cluster, project)
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/clusters/agents_resolver.rb b/app/graphql/resolvers/clusters/agents_resolver.rb
new file mode 100644
index 0000000000..9b8cea52e3
--- /dev/null
+++ b/app/graphql/resolvers/clusters/agents_resolver.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Clusters
+ class AgentsResolver < BaseResolver
+ include LooksAhead
+
+ type Types::Clusters::AgentType.connection_type, null: true
+
+ extras [:lookahead]
+
+ when_single do
+ argument :name, GraphQL::Types::String,
+ required: true,
+ description: 'Name of the cluster agent.'
+ end
+
+ alias_method :project, :object
+
+ def resolve_with_lookahead(**args)
+ apply_lookahead(
+ ::Clusters::AgentsFinder
+ .new(project, current_user, params: args)
+ .execute
+ )
+ end
+
+ private
+
+ def preloads
+ { tokens: :last_used_agent_tokens }
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
index 9de36b5b7d..855877110e 100644
--- a/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
+++ b/app/graphql/resolvers/concerns/issue_resolver_arguments.rb
@@ -4,6 +4,7 @@ module IssueResolverArguments
extend ActiveSupport::Concern
prepended do
+ include SearchArguments
include LooksAhead
argument :iid, GraphQL::Types::String,
@@ -49,9 +50,6 @@ module IssueResolverArguments
argument :closed_after, Types::TimeType,
required: false,
description: 'Issues closed after this date.'
- argument :search, GraphQL::Types::String,
- required: false,
- description: 'Search query for issue title or description.'
argument :types, [Types::IssueTypeEnum],
as: :issue_types,
description: 'Filter issues by the given issue types.',
@@ -62,6 +60,10 @@ module IssueResolverArguments
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user. Wildcard values "NONE" and "ANY" are supported.'
+ argument :confidential,
+ GraphQL::Types::Boolean,
+ required: false,
+ description: 'Filter for confidential issues. If "false", excludes confidential issues. If "true", returns only confidential issues.'
argument :not, Types::Issues::NegatedIssueFilterInputType,
description: 'Negated arguments.',
prepare: ->(negated_args, ctx) { negated_args.to_h },
@@ -91,6 +93,7 @@ module IssueResolverArguments
params_not_mutually_exclusive(args, mutually_exclusive_assignee_username_args)
params_not_mutually_exclusive(args, mutually_exclusive_milestone_args)
params_not_mutually_exclusive(args.fetch(:not, {}), mutually_exclusive_milestone_args)
+ validate_anonymous_search_access! if args[:search].present?
super
end
diff --git a/app/graphql/resolvers/concerns/search_arguments.rb b/app/graphql/resolvers/concerns/search_arguments.rb
new file mode 100644
index 0000000000..7f480f9d0b
--- /dev/null
+++ b/app/graphql/resolvers/concerns/search_arguments.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+module SearchArguments
+ extend ActiveSupport::Concern
+
+ included do
+ argument :search, GraphQL::Types::String,
+ required: false,
+ description: 'Search query for title or description.'
+ end
+
+ def validate_anonymous_search_access!
+ return if current_user.present? || Feature.disabled?(:disable_anonymous_search, type: :ops)
+
+ raise ::Gitlab::Graphql::Errors::ArgumentError,
+ "User must be authenticated to include the `search` argument."
+ end
+end
diff --git a/app/graphql/resolvers/issues_resolver.rb b/app/graphql/resolvers/issues_resolver.rb
index 47e4e3c0b3..b556964ae0 100644
--- a/app/graphql/resolvers/issues_resolver.rb
+++ b/app/graphql/resolvers/issues_resolver.rb
@@ -47,7 +47,8 @@ module Resolvers
alert_management_alert: [:alert_management_alert],
labels: [:labels],
assignees: [:assignees],
- timelogs: [:timelogs]
+ timelogs: [:timelogs],
+ customer_relations_contacts: { customer_relations_contacts: [:group] }
}
end
diff --git a/app/graphql/resolvers/kas/agent_configurations_resolver.rb b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
new file mode 100644
index 0000000000..238dae0bf1
--- /dev/null
+++ b/app/graphql/resolvers/kas/agent_configurations_resolver.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Kas
+ class AgentConfigurationsResolver < BaseResolver
+ type Types::Kas::AgentConfigurationType, null: true
+
+ # Calls Gitaly via KAS
+ calls_gitaly!
+
+ alias_method :project, :object
+
+ def resolve
+ return [] unless can_read_agent_configuration?
+
+ kas_client.list_agent_config_files(project: project)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
+ end
+
+ private
+
+ def can_read_agent_configuration?
+ current_user.can?(:admin_cluster, project)
+ end
+
+ def kas_client
+ @kas_client ||= Gitlab::Kas::Client.new
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/kas/agent_connections_resolver.rb b/app/graphql/resolvers/kas/agent_connections_resolver.rb
new file mode 100644
index 0000000000..8b7c400359
--- /dev/null
+++ b/app/graphql/resolvers/kas/agent_connections_resolver.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module Resolvers
+ module Kas
+ class AgentConnectionsResolver < BaseResolver
+ type Types::Kas::AgentConnectionType, null: true
+
+ alias_method :agent, :object
+
+ delegate :project, to: :agent
+
+ def resolve
+ return [] unless can_read_connected_agents?
+
+ BatchLoader::GraphQL.for(agent.id).batch(key: project, default_value: []) do |agent_ids, loader|
+ agents = get_connected_agents.group_by(&:agent_id).slice(*agent_ids)
+
+ agents.each do |agent_id, connections|
+ loader.call(agent_id, connections)
+ end
+ end
+ end
+
+ private
+
+ def can_read_connected_agents?
+ current_user.can?(:admin_cluster, project)
+ end
+
+ def get_connected_agents
+ kas_client.get_connected_agents(project: project)
+ rescue GRPC::BadStatus => e
+ raise Gitlab::Graphql::Errors::ResourceNotAvailable, e.class.name
+ end
+
+ def kas_client
+ @kas_client ||= Gitlab::Kas::Client.new
+ end
+ end
+ end
+end
diff --git a/app/graphql/resolvers/project_pipeline_resolver.rb b/app/graphql/resolvers/project_pipeline_resolver.rb
index ce4b6ac6b0..5acd7f9560 100644
--- a/app/graphql/resolvers/project_pipeline_resolver.rb
+++ b/app/graphql/resolvers/project_pipeline_resolver.rb
@@ -2,6 +2,8 @@
module Resolvers
class ProjectPipelineResolver < BaseResolver
+ include LooksAhead
+
type ::Types::Ci::PipelineType, null: true
alias_method :project, :object
@@ -14,7 +16,7 @@ module Resolvers
required: false,
description: 'SHA of the Pipeline. For example, "dyd0f15ay83993f5ab66k927w28673882x99100b".'
- def ready?(iid: nil, sha: nil)
+ def ready?(iid: nil, sha: nil, **args)
unless iid.present? ^ sha.present?
raise Gitlab::Graphql::Errors::ArgumentError, 'Provide one of an IID or SHA'
end
@@ -22,18 +24,21 @@ module Resolvers
super
end
- def resolve(iid: nil, sha: nil)
+ # the preloads are defined on ee/app/graphql/ee/resolvers/project_pipeline_resolver.rb
+ def resolve(iid: nil, sha: nil, **args)
+ self.lookahead = args.delete(:lookahead)
+
if iid
- BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader, args|
+ BatchLoader::GraphQL.for(iid).batch(key: project) do |iids, loader|
finder = ::Ci::PipelinesFinder.new(project, current_user, iids: iids)
- finder.execute.each { |pipeline| loader.call(pipeline.iid.to_s, pipeline) }
+ apply_lookahead(finder.execute).each { |pipeline| loader.call(pipeline.iid.to_s, pipeline) }
end
else
- BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader, args|
+ BatchLoader::GraphQL.for(sha).batch(key: project) do |shas, loader|
finder = ::Ci::PipelinesFinder.new(project, current_user, sha: shas)
- finder.execute.each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
+ apply_lookahead(finder.execute).each { |pipeline| loader.call(pipeline.sha.to_s, pipeline) }
end
end
end
diff --git a/app/graphql/resolvers/project_pipelines_resolver.rb b/app/graphql/resolvers/project_pipelines_resolver.rb
index 0171473a77..5a1e92efc9 100644
--- a/app/graphql/resolvers/project_pipelines_resolver.rb
+++ b/app/graphql/resolvers/project_pipelines_resolver.rb
@@ -26,3 +26,5 @@ module Resolvers
end
end
# rubocop: enable Graphql/ResolverType
+
+Resolvers::ProjectPipelinesResolver.prepend_mod
diff --git a/app/graphql/types/base_field.rb b/app/graphql/types/base_field.rb
index 9c27f0f813..93e17ea6df 100644
--- a/app/graphql/types/base_field.rb
+++ b/app/graphql/types/base_field.rb
@@ -9,7 +9,6 @@ module Types
DEFAULT_COMPLEXITY = 1
attr_reader :deprecation, :doc_reference
- attr_writer :max_page_size # Can be removed with :performance_roadmap feature flag: https://gitlab.com/gitlab-org/gitlab/-/issues/337198
def initialize(**kwargs, &block)
@calls_gitaly = !!kwargs.delete(:calls_gitaly)
@@ -21,6 +20,7 @@ module Types
@feature_flag = kwargs[:feature_flag]
kwargs = check_feature_flag(kwargs)
@deprecation = gitlab_deprecation(kwargs)
+ after_connection_extensions = kwargs.delete(:late_extensions) || []
super(**kwargs, &block)
@@ -28,6 +28,8 @@ module Types
extension ::Gitlab::Graphql::CallsGitaly::FieldExtension if Gitlab.dev_or_test_env?
extension ::Gitlab::Graphql::Present::FieldExtension
extension ::Gitlab::Graphql::Authorize::ConnectionFilterExtension
+
+ after_connection_extensions.each { extension _1 } if after_connection_extensions.any?
end
def may_call_gitaly?
diff --git a/app/graphql/types/board_list_type.rb b/app/graphql/types/board_list_type.rb
index 762e03973d..8c67803e39 100644
--- a/app/graphql/types/board_list_type.rb
+++ b/app/graphql/types/board_list_type.rb
@@ -10,8 +10,10 @@ module Types
alias_method :list, :object
- field :id, GraphQL::Types::ID, null: false,
+ field :id, GraphQL::Types::ID,
+ null: false,
description: 'ID (global ID) of the list.'
+
field :title, GraphQL::Types::String, null: false,
description: 'Title of the list.'
field :list_type, GraphQL::Types::String, null: false,
@@ -27,6 +29,7 @@ module Types
field :issues, ::Types::IssueType.connection_type, null: true,
description: 'Board issues.',
+ late_extensions: [Gitlab::Graphql::Board::IssuesConnectionExtension],
resolver: ::Resolvers::BoardListIssuesResolver
def issues_count
@@ -46,6 +49,16 @@ module Types
.metadata
end
end
+
+ # board lists have a data dependency on label - so we batch load them here
+ def title
+ BatchLoader::GraphQL.for(object).batch do |lists, callback|
+ ActiveRecord::Associations::Preloader.new.preload(lists, :label) # rubocop: disable CodeReuse/ActiveRecord
+
+ # all list titles are preloaded at this point
+ lists.each { |list| callback.call(list, list.title) }
+ end
+ end
end
# rubocop: enable Graphql/AuthorizeTypes
end
diff --git a/app/graphql/types/ci/runner_status_enum.rb b/app/graphql/types/ci/runner_status_enum.rb
index ad69175e44..8501ce2020 100644
--- a/app/graphql/types/ci/runner_status_enum.rb
+++ b/app/graphql/types/ci/runner_status_enum.rb
@@ -6,8 +6,21 @@ module Types
graphql_name 'CiRunnerStatus'
::Ci::Runner::AVAILABLE_STATUSES.each do |status|
+ description = case status
+ when 'active'
+ "A runner that is not paused."
+ when 'online'
+ "A runner that contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
+ when 'offline'
+ "A runner that has not contacted this instance within the last #{::Ci::Runner::ONLINE_CONTACT_TIMEOUT.inspect}."
+ when 'not_connected'
+ "A runner that has never contacted this instance."
+ else
+ "A runner that is #{status.to_s.tr('_', ' ')}."
+ end
+
value status.to_s.upcase,
- description: "A runner that is #{status.to_s.tr('_', ' ')}.",
+ description: description,
value: status.to_sym
end
end
diff --git a/app/graphql/types/ci/runner_type.rb b/app/graphql/types/ci/runner_type.rb
index e2c8070af0..9bf98aa7e8 100644
--- a/app/graphql/types/ci/runner_type.rb
+++ b/app/graphql/types/ci/runner_type.rb
@@ -3,10 +3,13 @@
module Types
module Ci
class RunnerType < BaseObject
+ edge_type_class(RunnerWebUrlEdge)
graphql_name 'CiRunner'
authorize :read_runner
present_using ::Ci::RunnerPresenter
+ expose_permissions Types::PermissionTypes::Ci::Runner
+
JOB_COUNT_LIMIT = 1000
alias_method :runner, :object
@@ -46,12 +49,18 @@ module Types
description: 'Number of projects that the runner is associated with.'
field :job_count, GraphQL::Types::Int, null: true,
description: "Number of jobs processed by the runner (limited to #{JOB_COUNT_LIMIT}, plus one to indicate that more items exist)."
+ field :admin_url, GraphQL::Types::String, null: true,
+ description: 'Admin URL of the runner. Only available for adminstrators.'
def job_count
# We limit to 1 above the JOB_COUNT_LIMIT to indicate that more items exist after JOB_COUNT_LIMIT
runner.builds.limit(JOB_COUNT_LIMIT + 1).count
end
+ def admin_url
+ Gitlab::Routing.url_helpers.admin_runner_url(runner) if can_admin_runners?
+ end
+
# rubocop: disable CodeReuse/ActiveRecord
def project_count
BatchLoader::GraphQL.for(runner.id).batch(key: :runner_project_count) do |ids, loader, args|
@@ -68,6 +77,12 @@ module Types
end
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ private
+
+ def can_admin_runners?
+ context[:current_user]&.can_admin_all_resources?
+ end
end
end
end
diff --git a/app/graphql/types/ci/runner_web_url_edge.rb b/app/graphql/types/ci/runner_web_url_edge.rb
new file mode 100644
index 0000000000..3b9fdfd157
--- /dev/null
+++ b/app/graphql/types/ci/runner_web_url_edge.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Types
+ module Ci
+ # rubocop: disable Graphql/AuthorizeTypes
+ class RunnerWebUrlEdge < GraphQL::Types::Relay::BaseEdge
+ include FindClosest
+
+ field :web_url, GraphQL::Types::String, null: true,
+ description: 'Web URL of the runner. The value depends on where you put this field in the query. You can use it for projects or groups.',
+ extras: [:parent]
+
+ def initialize(node, connection)
+ super
+
+ @runner = node.node
+ end
+
+ def web_url(parent:)
+ owner = closest_parent([::Types::ProjectType, ::Types::GroupType], parent)
+
+ case owner
+ when ::Group
+ Gitlab::Routing.url_helpers.group_runner_url(owner, @runner)
+ when ::Project
+ Gitlab::Routing.url_helpers.project_runner_url(owner, @runner)
+ end
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/clusters/agent_token_type.rb b/app/graphql/types/clusters/agent_token_type.rb
new file mode 100644
index 0000000000..94c5fc46a5
--- /dev/null
+++ b/app/graphql/types/clusters/agent_token_type.rb
@@ -0,0 +1,52 @@
+# frozen_string_literal: true
+
+module Types
+ module Clusters
+ class AgentTokenType < BaseObject
+ graphql_name 'ClusterAgentToken'
+
+ authorize :admin_cluster
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :cluster_agent,
+ Types::Clusters::AgentType,
+ description: 'Cluster agent this token is associated with.',
+ null: true
+
+ field :created_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the token was created.'
+
+ field :created_by_user,
+ Types::UserType,
+ null: true,
+ description: 'User who created the token.'
+
+ field :description,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Description of the token.'
+
+ field :last_used_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the token was last used.'
+
+ field :id,
+ ::Types::GlobalIDType[::Clusters::AgentToken],
+ null: false,
+ description: 'Global ID of the token.'
+
+ field :name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name given to the token.'
+
+ def cluster_agent
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(::Clusters::Agent, object.agent_id).find
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/clusters/agent_type.rb b/app/graphql/types/clusters/agent_type.rb
new file mode 100644
index 0000000000..ce748f6e8a
--- /dev/null
+++ b/app/graphql/types/clusters/agent_type.rb
@@ -0,0 +1,67 @@
+# frozen_string_literal: true
+
+module Types
+ module Clusters
+ class AgentType < BaseObject
+ graphql_name 'ClusterAgent'
+
+ authorize :admin_cluster
+
+ connection_type_class(Types::CountableConnectionType)
+
+ field :created_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the cluster agent was created.'
+
+ field :created_by_user,
+ Types::UserType,
+ null: true,
+ description: 'User object, containing information about the person who created the agent.'
+
+ field :id, GraphQL::Types::ID,
+ null: false,
+ description: 'ID of the cluster agent.'
+
+ field :name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the cluster agent.'
+
+ field :project, Types::ProjectType,
+ description: 'Project this cluster agent is associated with.',
+ null: true,
+ authorize: :read_project
+
+ field :tokens, Types::Clusters::AgentTokenType.connection_type,
+ description: 'Tokens associated with the cluster agent.',
+ null: true,
+ resolver: ::Resolvers::Clusters::AgentTokensResolver
+
+ field :updated_at,
+ Types::TimeType,
+ null: true,
+ description: 'Timestamp the cluster agent was updated.'
+
+ field :web_path,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Web path of the cluster agent.'
+
+ field :connections,
+ Types::Kas::AgentConnectionType.connection_type,
+ null: true,
+ description: 'Active connections for the cluster agent',
+ complexity: 5,
+ resolver: ::Resolvers::Kas::AgentConnectionsResolver
+
+ def project
+ Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
+ end
+
+ def web_path
+ ::Gitlab::Routing.url_helpers.project_cluster_agent_path(object.project, object.name)
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/concerns/find_closest.rb b/app/graphql/types/concerns/find_closest.rb
index 1d76e87236..3064db19ea 100644
--- a/app/graphql/types/concerns/find_closest.rb
+++ b/app/graphql/types/concerns/find_closest.rb
@@ -1,11 +1,15 @@
# frozen_string_literal: true
module FindClosest
- # Find the closest node of a given type above this node, and return the domain object
- def closest_parent(type, parent)
- parent = parent.try(:parent) while parent && parent.object.class != type
- return unless parent
+ # Find the closest node which has any of the given types above this node, and return the domain object
+ def closest_parent(types, parent)
+ while parent
- parent.object.object
+ if types.any? {|type| parent.object.instance_of? type}
+ return parent.object.object
+ else
+ parent = parent.try(:parent)
+ end
+ end
end
end
diff --git a/app/graphql/types/container_expiration_policy_older_than_enum.rb b/app/graphql/types/container_expiration_policy_older_than_enum.rb
index 7364910f8c..9c32d767ca 100644
--- a/app/graphql/types/container_expiration_policy_older_than_enum.rb
+++ b/app/graphql/types/container_expiration_policy_older_than_enum.rb
@@ -6,6 +6,7 @@ module Types
'7d': 'SEVEN_DAYS',
'14d': 'FOURTEEN_DAYS',
'30d': 'THIRTY_DAYS',
+ '60d': 'SIXTY_DAYS',
'90d': 'NINETY_DAYS'
}.freeze
diff --git a/app/graphql/types/container_repository_details_type.rb b/app/graphql/types/container_repository_details_type.rb
index 1a9f57e701..8190cc9bc2 100644
--- a/app/graphql/types/container_repository_details_type.rb
+++ b/app/graphql/types/container_repository_details_type.rb
@@ -17,5 +17,11 @@ module Types
def can_delete
Ability.allowed?(current_user, :destroy_container_image, object)
end
+
+ def tags
+ object.tags
+ rescue Faraday::Error
+ raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'We are having trouble connecting to the Container Registry. If this error persists, please review the troubleshooting documentation.'
+ end
end
end
diff --git a/app/graphql/types/container_repository_type.rb b/app/graphql/types/container_repository_type.rb
index 67093f5786..1fe5cf112f 100644
--- a/app/graphql/types/container_repository_type.rb
+++ b/app/graphql/types/container_repository_type.rb
@@ -28,5 +28,11 @@ module Types
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
end
+
+ def tags_count
+ object.tags_count
+ rescue Faraday::Error
+ raise ::Gitlab::Graphql::Errors::ResourceNotAvailable, 'We are having trouble connecting to the Container Registry. If this error persists, please review the troubleshooting documentation.'
+ end
end
end
diff --git a/app/graphql/types/customer_relations/contact_type.rb b/app/graphql/types/customer_relations/contact_type.rb
index 35b5bf4569..b5224a3e23 100644
--- a/app/graphql/types/customer_relations/contact_type.rb
+++ b/app/graphql/types/customer_relations/contact_type.rb
@@ -39,7 +39,7 @@ module Types
field :description,
GraphQL::Types::String,
null: true,
- description: 'Description or notes for the contact.'
+ description: 'Description of or notes for the contact.'
field :created_at,
Types::TimeType,
diff --git a/app/graphql/types/customer_relations/organization_type.rb b/app/graphql/types/customer_relations/organization_type.rb
index 0e091d4a9a..9b22fa35b1 100644
--- a/app/graphql/types/customer_relations/organization_type.rb
+++ b/app/graphql/types/customer_relations/organization_type.rb
@@ -25,7 +25,7 @@ module Types
field :description,
GraphQL::Types::String,
null: true,
- description: 'Description or notes for the organization.'
+ description: 'Description of or notes for the organization.'
field :created_at,
Types::TimeType,
diff --git a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
index 79e789d3f8..826ae61a1a 100644
--- a/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
+++ b/app/graphql/types/error_tracking/sentry_detailed_error_type.rb
@@ -13,6 +13,9 @@ module Types
field :id, GraphQL::Types::ID,
null: false,
description: 'ID (global ID) of the error.'
+ field :integrated, GraphQL::Types::Boolean,
+ null: true,
+ description: 'Error tracking backend.'
field :sentry_id, GraphQL::Types::String,
method: :id,
null: false,
diff --git a/app/graphql/types/group_type.rb b/app/graphql/types/group_type.rb
index 8fe4ba557e..b1bbabcdae 100644
--- a/app/graphql/types/group_type.rb
+++ b/app/graphql/types/group_type.rb
@@ -234,6 +234,10 @@ module Types
)
end
+ def dependency_proxy_setting
+ group.dependency_proxy_setting || group.create_dependency_proxy_setting
+ end
+
private
def group
diff --git a/app/graphql/types/issue_type.rb b/app/graphql/types/issue_type.rb
index c8db2b84ff..3b0f93d8dc 100644
--- a/app/graphql/types/issue_type.rb
+++ b/app/graphql/types/issue_type.rb
@@ -136,6 +136,9 @@ module Types
field :project_id, GraphQL::Types::Int, null: false, method: :project_id,
description: 'ID of the issue project.'
+ field :customer_relations_contacts, Types::CustomerRelations::ContactType.connection_type, null: true,
+ description: 'Customer relations contacts of the issue.'
+
def author
Gitlab::Graphql::Loaders::BatchModelLoader.new(User, object.author_id).find
end
diff --git a/app/graphql/types/issues/negated_issue_filter_input_type.rb b/app/graphql/types/issues/negated_issue_filter_input_type.rb
index 4f620a5b3d..c8b7cdaa68 100644
--- a/app/graphql/types/issues/negated_issue_filter_input_type.rb
+++ b/app/graphql/types/issues/negated_issue_filter_input_type.rb
@@ -29,6 +29,10 @@ module Types
argument :my_reaction_emoji, GraphQL::Types::String,
required: false,
description: 'Filter by reaction emoji applied by the current user.'
+ argument :types, [Types::IssueTypeEnum],
+ as: :issue_types,
+ description: 'Filters out issues by the given issue types.',
+ required: false
end
end
end
diff --git a/app/graphql/types/kas/agent_configuration_type.rb b/app/graphql/types/kas/agent_configuration_type.rb
new file mode 100644
index 0000000000..397a573967
--- /dev/null
+++ b/app/graphql/types/kas/agent_configuration_type.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentConfigurationType < BaseObject
+ graphql_name 'AgentConfiguration'
+ description 'Configuration details for an Agent'
+
+ field :agent_name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the agent.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/kas/agent_connection_type.rb b/app/graphql/types/kas/agent_connection_type.rb
new file mode 100644
index 0000000000..9c6321bece
--- /dev/null
+++ b/app/graphql/types/kas/agent_connection_type.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentConnectionType < BaseObject
+ graphql_name 'ConnectedAgent'
+ description 'Connection details for an Agent'
+
+ field :connected_at,
+ Types::TimeType,
+ null: true,
+ description: 'When the connection was established.'
+
+ field :connection_id,
+ GraphQL::Types::BigInt,
+ null: true,
+ description: 'ID of the connection.'
+
+ field :metadata,
+ Types::Kas::AgentMetadataType,
+ method: :agent_meta,
+ null: true,
+ description: 'Information about the Agent.'
+
+ def connected_at
+ Time.at(object.connected_at.seconds)
+ end
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/kas/agent_metadata_type.rb b/app/graphql/types/kas/agent_metadata_type.rb
new file mode 100644
index 0000000000..4a3bb09b9e
--- /dev/null
+++ b/app/graphql/types/kas/agent_metadata_type.rb
@@ -0,0 +1,33 @@
+# frozen_string_literal: true
+
+module Types
+ module Kas
+ # rubocop: disable Graphql/AuthorizeTypes
+ class AgentMetadataType < BaseObject
+ graphql_name 'AgentMetadata'
+ description 'Information about a connected Agent'
+
+ field :version,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Agent version tag.'
+
+ field :commit,
+ GraphQL::Types::String,
+ method: :commit_id,
+ null: true,
+ description: 'Agent version commit.'
+
+ field :pod_namespace,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Namespace of the pod running the Agent.'
+
+ field :pod_name,
+ GraphQL::Types::String,
+ null: true,
+ description: 'Name of the pod running the Agent.'
+ end
+ # rubocop: enable Graphql/AuthorizeTypes
+ end
+end
diff --git a/app/graphql/types/merge_requests/interacts_with_merge_request.rb b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
index d685ac4d3c..d4a1f2faa8 100644
--- a/app/graphql/types/merge_requests/interacts_with_merge_request.rb
+++ b/app/graphql/types/merge_requests/interacts_with_merge_request.rb
@@ -14,7 +14,7 @@ module Types
end
def merge_request_interaction(parent:)
- merge_request = closest_parent(::Types::MergeRequestType, parent)
+ merge_request = closest_parent([::Types::MergeRequestType], parent)
return unless merge_request
Users::MergeRequestInteraction.new(user: object, merge_request: merge_request)
diff --git a/app/graphql/types/milestone_wildcard_id_enum.rb b/app/graphql/types/milestone_wildcard_id_enum.rb
index 12e8e07fb0..ad9651a26d 100644
--- a/app/graphql/types/milestone_wildcard_id_enum.rb
+++ b/app/graphql/types/milestone_wildcard_id_enum.rb
@@ -8,6 +8,6 @@ module Types
value 'NONE', 'No milestone is assigned.'
value 'ANY', 'Milestone is assigned.'
value 'STARTED', 'Milestone assigned is open and started (start date <= today).'
- value 'UPCOMING', 'Milestone assigned is due closest in the future (due date > today).'
+ value 'UPCOMING', 'Milestone assigned is due in the future (due date > today).'
end
end
diff --git a/app/graphql/types/mutation_type.rb b/app/graphql/types/mutation_type.rb
index ea50af1c55..cd4c45d294 100644
--- a/app/graphql/types/mutation_type.rb
+++ b/app/graphql/types/mutation_type.rb
@@ -31,13 +31,20 @@ module Types
mount_mutation Mutations::Boards::Lists::Update
mount_mutation Mutations::Boards::Lists::Destroy
mount_mutation Mutations::Branches::Create, calls_gitaly: true
+ mount_mutation Mutations::Clusters::Agents::Create
+ mount_mutation Mutations::Clusters::Agents::Delete
+ mount_mutation Mutations::Clusters::AgentTokens::Create
+ mount_mutation Mutations::Clusters::AgentTokens::Delete
mount_mutation Mutations::Commits::Create, calls_gitaly: true
mount_mutation Mutations::CustomEmoji::Create, feature_flag: :custom_emoji
mount_mutation Mutations::CustomEmoji::Destroy, feature_flag: :custom_emoji
+ mount_mutation Mutations::CustomerRelations::Contacts::Create
+ mount_mutation Mutations::CustomerRelations::Contacts::Update
mount_mutation Mutations::CustomerRelations::Organizations::Create
mount_mutation Mutations::CustomerRelations::Organizations::Update
mount_mutation Mutations::Discussions::ToggleResolve
mount_mutation Mutations::DependencyProxy::ImageTtlGroupPolicy::Update
+ mount_mutation Mutations::DependencyProxy::GroupSettings::Update
mount_mutation Mutations::Environments::CanaryIngress::Update
mount_mutation Mutations::Issues::Create
mount_mutation Mutations::Issues::SetAssignees
diff --git a/app/graphql/types/packages/nuget/metadatum_type.rb b/app/graphql/types/packages/nuget/metadatum_type.rb
index ed9d97724a..b58fd954a7 100644
--- a/app/graphql/types/packages/nuget/metadatum_type.rb
+++ b/app/graphql/types/packages/nuget/metadatum_type.rb
@@ -10,9 +10,9 @@ module Types
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Nuget::Metadatum], null: false, description: 'ID of the metadatum.'
- field :license_url, GraphQL::Types::String, null: false, description: 'License URL of the Nuget package.'
- field :project_url, GraphQL::Types::String, null: false, description: 'Project URL of the Nuget package.'
- field :icon_url, GraphQL::Types::String, null: false, description: 'Icon URL of the Nuget package.'
+ field :license_url, GraphQL::Types::String, null: true, description: 'License URL of the Nuget package.'
+ field :project_url, GraphQL::Types::String, null: true, description: 'Project URL of the Nuget package.'
+ field :icon_url, GraphQL::Types::String, null: true, description: 'Icon URL of the Nuget package.'
end
end
end
diff --git a/app/graphql/types/packages/package_type.rb b/app/graphql/types/packages/package_type.rb
index f3fa79cc08..9851c6aec7 100644
--- a/app/graphql/types/packages/package_type.rb
+++ b/app/graphql/types/packages/package_type.rb
@@ -6,6 +6,8 @@ module Types
graphql_name 'Package'
description 'Represents a package in the Package Registry. Note that this type is in beta and susceptible to changes'
+ connection_type_class(Types::CountableConnectionType)
+
authorize :read_package
field :id, ::Types::GlobalIDType[::Packages::Package], null: false,
@@ -26,6 +28,7 @@ module Types
description: 'Other versions of the package.',
deprecated: { reason: 'This field is now only returned in the PackageDetailsType', milestone: '13.11' }
field :status, Types::Packages::PackageStatusEnum, null: false, description: 'Package status.'
+ field :can_destroy, GraphQL::Types::Boolean, null: false, description: 'Whether the user can destroy the package.'
def project
Gitlab::Graphql::Loaders::BatchModelLoader.new(Project, object.project_id).find
@@ -35,6 +38,10 @@ module Types
[]
end
+ def can_destroy
+ Ability.allowed?(current_user, :destroy_package, object)
+ end
+
# NOTE: This method must be kept in sync with the union
# type: `Types::Packages::MetadataType`.
#
diff --git a/app/graphql/types/permission_types/ci/runner.rb b/app/graphql/types/permission_types/ci/runner.rb
new file mode 100644
index 0000000000..2e92a4011e
--- /dev/null
+++ b/app/graphql/types/permission_types/ci/runner.rb
@@ -0,0 +1,13 @@
+# frozen_string_literal: true
+
+module Types
+ module PermissionTypes
+ module Ci
+ class Runner < BasePermissionType
+ graphql_name 'RunnerPermissions'
+
+ abilities :read_runner, :update_runner, :delete_runner
+ end
+ end
+ end
+end
diff --git a/app/graphql/types/project_type.rb b/app/graphql/types/project_type.rb
index aef46a05a2..791875242d 100644
--- a/app/graphql/types/project_type.rb
+++ b/app/graphql/types/project_type.rb
@@ -208,6 +208,7 @@ module Types
Types::Ci::PipelineType,
null: true,
description: 'Build pipeline of the project.',
+ extras: [:lookahead],
resolver: Resolvers::ProjectPipelineResolver
field :ci_cd_settings,
@@ -361,6 +362,25 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
+ field :agent_configurations,
+ ::Types::Kas::AgentConfigurationType.connection_type,
+ null: true,
+ description: 'Agent configurations defined by the project',
+ resolver: ::Resolvers::Kas::AgentConfigurationsResolver
+
+ field :cluster_agent,
+ ::Types::Clusters::AgentType,
+ null: true,
+ description: 'Find a single cluster agent by name.',
+ resolver: ::Resolvers::Clusters::AgentsResolver.single
+
+ field :cluster_agents,
+ ::Types::Clusters::AgentType.connection_type,
+ extras: [:lookahead],
+ null: true,
+ description: 'Cluster agents associated with the project.',
+ resolver: ::Resolvers::Clusters::AgentsResolver
+
def label(title:)
BatchLoader::GraphQL.for(title).batch(key: project) do |titles, loader, args|
LabelsFinder
diff --git a/app/graphql/types/query_type.rb b/app/graphql/types/query_type.rb
index e02191fbf3..ed4ddbb982 100644
--- a/app/graphql/types/query_type.rb
+++ b/app/graphql/types/query_type.rb
@@ -136,6 +136,10 @@ module Types
complexity: 5,
resolver: ::Resolvers::TimelogResolver
+ field :board_list, ::Types::BoardListType,
+ null: true,
+ resolver: Resolvers::BoardListResolver
+
def design_management
DesignManagementObject.new(nil)
end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index cf15433f2e..2103a37180 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -333,6 +333,9 @@ module ApplicationSettingsHelper
:throttle_authenticated_files_api_enabled,
:throttle_authenticated_files_api_period_in_seconds,
:throttle_authenticated_files_api_requests_per_period,
+ :throttle_authenticated_deprecated_api_enabled,
+ :throttle_authenticated_deprecated_api_period_in_seconds,
+ :throttle_authenticated_deprecated_api_requests_per_period,
:throttle_unauthenticated_api_enabled,
:throttle_unauthenticated_api_period_in_seconds,
:throttle_unauthenticated_api_requests_per_period,
@@ -345,6 +348,9 @@ module ApplicationSettingsHelper
:throttle_unauthenticated_files_api_enabled,
:throttle_unauthenticated_files_api_period_in_seconds,
:throttle_unauthenticated_files_api_requests_per_period,
+ :throttle_unauthenticated_deprecated_api_enabled,
+ :throttle_unauthenticated_deprecated_api_period_in_seconds,
+ :throttle_unauthenticated_deprecated_api_requests_per_period,
:throttle_protected_paths_enabled,
:throttle_protected_paths_period_in_seconds,
:throttle_protected_paths_requests_per_period,
@@ -400,7 +406,8 @@ module ApplicationSettingsHelper
:user_deactivation_emails_enabled,
:sidekiq_job_limiter_mode,
:sidekiq_job_limiter_compression_threshold_bytes,
- :sidekiq_job_limiter_limit_bytes
+ :sidekiq_job_limiter_limit_bytes,
+ :suggest_pipeline_enabled
].tap do |settings|
settings << :deactivate_dormant_users unless Gitlab.com?
end
@@ -464,10 +471,6 @@ module ApplicationSettingsHelper
}
end
- def show_documentation_base_url_field?
- Feature.enabled?(:help_page_documentation_redirect)
- end
-
def valid_runner_registrars
Gitlab::CurrentSettings.valid_runner_registrars
end
@@ -477,8 +480,6 @@ module ApplicationSettingsHelper
end
def pending_user_count
- return 0 if Gitlab::CurrentSettings.new_user_signups_cap.blank?
-
User.blocked_pending_approval.count
end
end
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index 4cfa1528d9..dd852a6868 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -9,6 +9,10 @@ module AvatarsHelper
source_icon(group, options)
end
+ def topic_icon(topic, options = {})
+ source_icon(topic, options)
+ end
+
# Takes both user and email and returns the avatar_icon by
# user (preferred) or email.
def avatar_icon_for(user = nil, email = nil, size = nil, scale = 2, only_path: true)
diff --git a/app/helpers/ci/jobs_helper.rb b/app/helpers/ci/jobs_helper.rb
index 882302f05a..d02fe3f20b 100644
--- a/app/helpers/ci/jobs_helper.rb
+++ b/app/helpers/ci/jobs_helper.rb
@@ -7,7 +7,7 @@ module Ci
"endpoint" => project_job_path(@project, @build, format: :json),
"project_path" => @project.full_path,
"artifact_help_url" => help_page_path('user/gitlab_com/index.html', anchor: 'gitlab-cicd'),
- "deployment_help_url" => help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting'),
+ "deployment_help_url" => help_page_path('user/project/clusters/deploy_to_cluster.html', anchor: 'troubleshooting'),
"runner_settings_url" => project_runners_path(@build.project, anchor: 'js-runners-settings'),
"page_path" => project_job_path(@project, @build),
"build_status" => @build.status,
diff --git a/app/helpers/ci/runners_helper.rb b/app/helpers/ci/runners_helper.rb
index c9231a4eff..ec10610714 100644
--- a/app/helpers/ci/runners_helper.rb
+++ b/app/helpers/ci/runners_helper.rb
@@ -77,7 +77,7 @@ module Ci
def toggle_shared_runners_settings_data(project)
{
is_enabled: "#{project.shared_runners_enabled?}",
- is_disabled_and_unoverridable: "#{project.group&.shared_runners_setting == 'disabled_and_unoverridable'}",
+ is_disabled_and_unoverridable: "#{project.group&.shared_runners_setting == Namespace::SR_DISABLED_AND_UNOVERRIDABLE}",
update_path: toggle_shared_runners_project_runners_path(project)
}
end
diff --git a/app/helpers/commits_helper.rb b/app/helpers/commits_helper.rb
index 53017beee8..ee5f4bb364 100644
--- a/app/helpers/commits_helper.rb
+++ b/app/helpers/commits_helper.rb
@@ -17,6 +17,15 @@ module CommitsHelper
commit_person_link(commit, options.merge(source: :committer))
end
+ def commit_committer_avatar(committer, options = {})
+ user_avatar(options.merge({
+ user: committer,
+ user_name: committer.name,
+ user_email: committer.email,
+ css_class: 'd-none d-sm-inline-block float-none gl-mr-0! gl-vertical-align-text-bottom'
+ }))
+ end
+
def commit_to_html(commit, ref, project)
render 'projects/commits/commit.html',
commit: commit,
diff --git a/app/helpers/feature_flags_helper.rb b/app/helpers/feature_flags_helper.rb
index 2b8804bc07..e12c6c605d 100644
--- a/app/helpers/feature_flags_helper.rb
+++ b/app/helpers/feature_flags_helper.rb
@@ -11,8 +11,15 @@ module FeatureFlagsHelper
project.feature_flags_client_token
end
- def feature_flag_issues_links_endpoint(_project, _feature_flag, _user)
- ''
+ def edit_feature_flag_data
+ {
+ endpoint: project_feature_flag_path(@project, @feature_flag),
+ project_id: @project.id,
+ feature_flags_path: project_feature_flags_path(@project),
+ environments_endpoint: search_project_environments_path(@project, format: :json),
+ strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
+ environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-environments-with-specs')
+ }
end
end
diff --git a/app/helpers/groups_helper.rb b/app/helpers/groups_helper.rb
index a24776eb2e..30aaa0a5ac 100644
--- a/app/helpers/groups_helper.rb
+++ b/app/helpers/groups_helper.rb
@@ -109,7 +109,7 @@ module GroupsHelper
end
def prevent_sharing_groups_outside_hierarchy_help_text(group)
- s_("GroupSettings|This setting is only available on the top-level group and it applies to all subgroups. Groups that have already been shared with a group outside %{group} will still be shared, and this access will have to be revoked manually.").html_safe % { group: link_to_group(group) }
+ s_("GroupSettings|Available only on the top-level group. Applies to all subgroups. Groups already shared with a group outside %{group} are still shared unless removed manually.").html_safe % { group: link_to_group(group) }
end
def parent_group_options(current_group)
@@ -178,7 +178,7 @@ module GroupsHelper
end
def default_help
- s_("GroupSettings|This setting will be applied to all subgroups unless overridden by a group owner. Groups that already have access to the project will continue to have access unless removed manually.")
+ s_("GroupSettings|Applied to all subgroups unless overridden by a group owner. Groups already added to the project lose access.")
end
def ancestor_locked_but_you_can_override(group)
diff --git a/app/helpers/hooks_helper.rb b/app/helpers/hooks_helper.rb
index 2725d28c47..c1dfd2b2cd 100644
--- a/app/helpers/hooks_helper.rb
+++ b/app/helpers/hooks_helper.rb
@@ -36,6 +36,15 @@ module HooksHelper
admin_hook_path(hook)
end
end
+
+ def hook_log_path(hook, hook_log)
+ case hook
+ when ProjectHook
+ hook_log.present.details_path
+ when SystemHook
+ admin_hook_hook_log_path(hook, hook_log)
+ end
+ end
end
HooksHelper.prepend_mod_with('HooksHelper')
diff --git a/app/helpers/integrations_helper.rb b/app/helpers/integrations_helper.rb
index 904508867d..8819aa9e9c 100644
--- a/app/helpers/integrations_helper.rb
+++ b/app/helpers/integrations_helper.rb
@@ -125,15 +125,6 @@ module IntegrationsHelper
!Gitlab.com?
end
- def integration_tabs(integration:)
- [
- { key: 'edit', text: _('Settings'), href: scoped_edit_integration_path(integration) },
- (
- { key: 'overrides', text: s_('Integrations|Projects using custom settings'), href: scoped_overrides_integration_path(integration) } if integration.instance_level?
- )
- ].compact
- end
-
def jira_issue_breadcrumb_link(issue_reference)
link_to '', { class: 'gl-display-flex gl-align-items-center gl-white-space-nowrap' } do
icon = image_tag image_path('illustrations/logos/jira.svg'), width: 15, height: 15, class: 'gl-mr-2'
diff --git a/app/helpers/issuables_helper.rb b/app/helpers/issuables_helper.rb
index f3cc46216e..24c6ef8cd6 100644
--- a/app/helpers/issuables_helper.rb
+++ b/app/helpers/issuables_helper.rb
@@ -198,7 +198,7 @@ module IssuablesHelper
if count != -1
html << " " << content_tag(:span,
format_count(issuable_type, count, Gitlab::IssuablesCountForState::THRESHOLD),
- class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm'
+ class: 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex'
)
end
diff --git a/app/helpers/issues_helper.rb b/app/helpers/issues_helper.rb
index 40e86b4623..49f7d9aeef 100644
--- a/app/helpers/issues_helper.rb
+++ b/app/helpers/issues_helper.rb
@@ -238,9 +238,10 @@ module IssuesHelper
)
end
- def group_issues_list_data(group, current_user, issues)
+ def group_issues_list_data(group, current_user, issues, projects)
common_issues_list_data(group, current_user).merge(
- has_any_issues: issues.to_a.any?.to_s
+ has_any_issues: issues.to_a.any?.to_s,
+ has_any_projects: any_projects?(projects).to_s
)
end
diff --git a/app/helpers/one_trust_helper.rb b/app/helpers/one_trust_helper.rb
new file mode 100644
index 0000000000..9f92a73a4d
--- /dev/null
+++ b/app/helpers/one_trust_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module OneTrustHelper
+ def one_trust_enabled?
+ Feature.enabled?(:ecomm_instrumentation, type: :ops) &&
+ Gitlab.config.extra.has_key?('one_trust_id') &&
+ Gitlab.config.extra.one_trust_id.present? &&
+ !current_user
+ end
+end
diff --git a/app/helpers/packages_helper.rb b/app/helpers/packages_helper.rb
index ebf30fb353..c69d9eb132 100644
--- a/app/helpers/packages_helper.rb
+++ b/app/helpers/packages_helper.rb
@@ -41,6 +41,7 @@ module PackagesHelper
def packages_list_data(type, resource)
{
resource_id: resource.id,
+ full_path: resource.full_path,
page_type: type,
empty_list_help_url: help_page_path('user/packages/package_registry/index'),
empty_list_illustration: image_path('illustrations/no-packages.svg'),
@@ -70,6 +71,7 @@ module PackagesHelper
can_delete: can?(current_user, :destroy_package, project).to_s,
svg_path: image_path('illustrations/no-packages.svg'),
npm_path: package_registry_instance_url(:npm),
+ npm_project_path: package_registry_project_url(project.id, :npm),
npm_help_path: help_page_path('user/packages/npm_registry/index'),
maven_path: package_registry_project_url(project.id, :maven),
maven_help_path: help_page_path('user/packages/maven_repository/index'),
diff --git a/app/helpers/projects/cluster_agents_helper.rb b/app/helpers/projects/cluster_agents_helper.rb
new file mode 100644
index 0000000000..20fa721cc3
--- /dev/null
+++ b/app/helpers/projects/cluster_agents_helper.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+module Projects::ClusterAgentsHelper
+ def js_cluster_agent_details_data(agent_name, project)
+ {
+ agent_name: agent_name,
+ project_path: project.full_path
+ }
+ end
+end
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 6f8fce1b4d..e3b63d122d 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -20,16 +20,15 @@ module ProjectsHelper
end
def link_to_member_avatar(author, opts = {})
- default_opts = { size: 16, lazy_load: false }
+ default_opts = { size: 16 }
opts = default_opts.merge(opts)
classes = %W[avatar avatar-inline s#{opts[:size]}]
classes << opts[:avatar_class] if opts[:avatar_class]
avatar = avatar_icon_for_user(author, opts[:size])
- src = opts[:lazy_load] ? nil : avatar
- image_tag(src, width: opts[:size], class: classes, alt: '', "data-src" => avatar)
+ image_tag(avatar, width: opts[:size], class: classes, alt: '')
end
def author_content_tag(author, opts = {})
@@ -351,7 +350,7 @@ module ProjectsHelper
end
def show_terraform_banner?(project)
- project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty?
+ Feature.enabled?(:show_terraform_banner, type: :ops, default_enabled: true) && project.repository_languages.with_programming_language('HCL').exists? && project.terraform_states.empty?
end
def project_permissions_panel_data(project)
diff --git a/app/helpers/routing/pseudonymization_helper.rb b/app/helpers/routing/pseudonymization_helper.rb
index 1d9320f010..b73e49803a 100644
--- a/app/helpers/routing/pseudonymization_helper.rb
+++ b/app/helpers/routing/pseudonymization_helper.rb
@@ -6,7 +6,8 @@ module Routing
return unless Feature.enabled?(:mask_page_urls, type: :ops)
mask_params(Rails.application.routes.recognize_path(request.original_fullpath))
- rescue ActionController::RoutingError, URI::InvalidURIError
+ rescue ActionController::RoutingError, URI::InvalidURIError => e
+ Gitlab::ErrorTracking.track_exception(e, url: request.original_fullpath)
nil
end
@@ -27,7 +28,7 @@ module Routing
when 'groups'
"/namespace:#{group.id}"
when 'projects'
- "/namespace:#{project.namespace.id}/project:#{project.id}"
+ "/namespace:#{project.namespace_id}/project:#{project.id}"
when 'root'
''
else
@@ -43,7 +44,7 @@ module Routing
masked_url = "#{request.protocol}#{request.host_with_port}"
if request_params.has_key?(:project_id)
- masked_url += "/namespace:#{project.namespace.id}/project:#{project.id}/-/#{namespace_type}"
+ masked_url += "/namespace:#{project.namespace_id}/project:#{project.id}/-/#{namespace_type}"
end
if request_params.has_key?(:id)
diff --git a/app/helpers/search_helper.rb b/app/helpers/search_helper.rb
index b8e58e3afb..cb28025c90 100644
--- a/app/helpers/search_helper.rb
+++ b/app/helpers/search_helper.rb
@@ -87,9 +87,9 @@ module SearchHelper
def search_entries_info_template(collection)
if collection.total_pages > 1
- s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for%{term_element}").html_safe
+ s_("SearchResults|Showing %{from} - %{to} of %{count} %{scope} for %{term_element}").html_safe
else
- s_("SearchResults|Showing %{count} %{scope} for%{term_element}").html_safe
+ s_("SearchResults|Showing %{count} %{scope} for %{term_element}").html_safe
end
end
diff --git a/app/helpers/startupjs_helper.rb b/app/helpers/startupjs_helper.rb
index b595590c7c..2e8f0cb7db 100644
--- a/app/helpers/startupjs_helper.rb
+++ b/app/helpers/startupjs_helper.rb
@@ -5,6 +5,13 @@ module StartupjsHelper
@graphql_startup_calls
end
+ def page_startup_graphql_headers
+ {
+ 'X-CSRF-Token' => form_authenticity_token,
+ 'x-gitlab-feature-category' => ::Gitlab::ApplicationContext.current_context_attribute(:feature_category).presence || ''
+ }
+ end
+
def add_page_startup_graphql_call(query, variables = {})
@graphql_startup_calls ||= []
file_location = File.join(Rails.root, "app/graphql/queries/#{query}.query.graphql")
diff --git a/app/helpers/tab_helper.rb b/app/helpers/tab_helper.rb
index e64e1c935d..a6bb2f3b24 100644
--- a/app/helpers/tab_helper.rb
+++ b/app/helpers/tab_helper.rb
@@ -1,6 +1,67 @@
# frozen_string_literal: true
module TabHelper
+ # Navigation tabs helper
+
+ # Create a
container
+ #
+ # Returns a `ul` element with classes that correspond to
+ # the component. Can be populated by
+ # gl_tab_link_to elements.
+ #
+ # See more at: https://gitlab-org.gitlab.io/gitlab-ui/?path=/story/base-tabs-tab--default
+ def gl_tabs_nav(html_options = {}, &block)
+ gl_tabs_classes = %w[nav gl-tabs-nav]
+
+ html_options = html_options.merge(
+ class: [*html_options[:class], gl_tabs_classes].join(' '),
+ role: 'tablist'
+ )
+
+ content = capture(&block) if block_given?
+ content_tag(:ul, content, html_options)
+ end
+
+ # Create a link
+ #
+ # When a tab is active it gets highlighted to indicate this is currently viewed tab.
+ # Internally `current_page?` is called to determine if this is the current tab.
+ #
+ # Usage is the same as "link_to", with the following additional options:
+ #
+ # html_options - The html_options hash (default: {})
+ # :item_active - Overrides the default state focing the "active" css classes (optional).
+ #
+ def gl_tab_link_to(name = nil, options = {}, html_options = {}, &block)
+ tab_class = 'nav-item'
+ link_classes = %w[nav-link gl-tab-nav-item]
+ active_link_classes = %w[active gl-tab-nav-item-active gl-tab-nav-item-active-indigo]
+
+ if block_given?
+ # Shift params to skip the omitted "name" param
+ html_options = options
+ options = name
+ end
+
+ html_options = html_options.merge(
+ class: [*html_options[:class], link_classes].join(' ')
+ )
+
+ if gl_tab_link_to_active?(options, html_options)
+ html_options[:class] = [*html_options[:class], active_link_classes].join(' ')
+ end
+
+ html_options = html_options.except(:item_active)
+
+ content_tag(:li, class: tab_class, role: 'presentation') do
+ if block_given?
+ link_to(options, html_options, &block)
+ else
+ link_to(name, options, html_options)
+ end
+ end
+ end
+
# Navigation link helper
#
# Returns an `li` element with an 'active' class if the supplied
@@ -12,7 +73,6 @@ module TabHelper
# :action - One or more action names to check (optional).
# :path - A shorthand path, such as 'dashboard#index', to check (optional).
# :html_options - Extra options to be passed to the list element (optional).
- # :unless - Callable object to skip rendering the 'active' class on `li` element (optional).
# block - An optional block that will become the contents of the returned
# `li` element.
#
@@ -57,11 +117,6 @@ module TabHelper
# nav_link(path: 'admin/appearances#show') { "Hello"}
# # => 'Hello'
#
- # # Shorthand path + unless
- # # Add `active` class when TreeController is requested, except the `index` action.
- # nav_link(controller: 'tree', unless: -> { action_name?('index') }) { "Hello" }
- # # => 'Hello'
- #
# # When `TreeController#index` is requested
# # => 'Hello'
#
@@ -90,8 +145,6 @@ module TabHelper
end
def active_nav_link?(options)
- return false if options[:unless]&.call
-
controller = options.delete(:controller)
action = options.delete(:action)
@@ -148,4 +201,12 @@ module TabHelper
current_controller?(*controller) || current_action?(*action)
end
end
+
+ def gl_tab_link_to_active?(options, html_options)
+ if html_options.has_key?(:item_active)
+ return html_options[:item_active]
+ end
+
+ current_page?(options)
+ end
end
diff --git a/app/helpers/time_zone_helper.rb b/app/helpers/time_zone_helper.rb
index f92e32ff9b..a0d9c8403e 100644
--- a/app/helpers/time_zone_helper.rb
+++ b/app/helpers/time_zone_helper.rb
@@ -33,6 +33,8 @@ module TimeZoneHelper
end
def local_time(timezone)
+ return if timezone.blank?
+
time_zone_instance = ActiveSupport::TimeZone.new(timezone) || Time.zone
time_zone_instance.now.strftime("%-l:%M %p")
end
diff --git a/app/helpers/timeboxes_helper.rb b/app/helpers/timeboxes_helper.rb
index 0993e210f4..eca4057273 100644
--- a/app/helpers/timeboxes_helper.rb
+++ b/app/helpers/timeboxes_helper.rb
@@ -78,19 +78,6 @@ module TimeboxesHelper
end
# rubocop: enable CodeReuse/ActiveRecord
- # Show 'active' class if provided GET param matches check
- # `or_blank` allows the function to return 'active' when given an empty param
- # Could be refactored to be simpler but that may make it harder to read
- def milestone_class_for_state(param, check, match_blank_param = false)
- if match_blank_param
- 'active' if param.blank? || param == check
- elsif param == check
- 'active'
- else
- check
- end
- end
-
def milestone_progress_tooltip_text(milestone)
has_issues = milestone.total_issues_count > 0
diff --git a/app/helpers/user_callouts_helper.rb b/app/helpers/user_callouts_helper.rb
index 2c3dc243d8..1c67ca983f 100644
--- a/app/helpers/user_callouts_helper.rb
+++ b/app/helpers/user_callouts_helper.rb
@@ -10,6 +10,7 @@ module UserCalloutsHelper
REGISTRATION_ENABLED_CALLOUT = 'registration_enabled_callout'
UNFINISHED_TAG_CLEANUP_CALLOUT = 'unfinished_tag_cleanup_callout'
INVITE_MEMBERS_BANNER = 'invite_members_banner'
+ SECURITY_NEWSLETTER_CALLOUT = 'security_newsletter_callout'
def show_gke_cluster_integration_callout?(project)
active_nav_link?(controller: sidebar_operations_paths) &&
@@ -64,6 +65,11 @@ module UserCalloutsHelper
!multiple_members?(group)
end
+ def show_security_newsletter_user_callout?
+ current_user&.admin? &&
+ !user_dismissed?(SECURITY_NEWSLETTER_CALLOUT)
+ end
+
private
def user_dismissed?(feature_name, ignore_dismissal_earlier_than = nil)
diff --git a/app/helpers/workhorse_helper.rb b/app/helpers/workhorse_helper.rb
index 8785c4cdcb..4862282bc7 100644
--- a/app/helpers/workhorse_helper.rb
+++ b/app/helpers/workhorse_helper.rb
@@ -41,6 +41,15 @@ module WorkhorseHelper
head :ok
end
+ def send_dependency(token, url, filename)
+ headers.store(*Gitlab::Workhorse.send_dependency(token, url))
+ headers['Content-Disposition'] =
+ ActionDispatch::Http::ContentDisposition.format(disposition: 'attachment', filename: filename)
+ headers['Content-Type'] = 'application/gzip'
+
+ head :ok
+ end
+
def set_workhorse_internal_api_content_type
headers['Content-Type'] = Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE
end
diff --git a/app/models/analytics/cycle_analytics/issue_stage_event.rb b/app/models/analytics/cycle_analytics/issue_stage_event.rb
index 1da8973ff2..3e6ed86d53 100644
--- a/app/models/analytics/cycle_analytics/issue_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/issue_stage_event.rb
@@ -3,9 +3,14 @@
module Analytics
module CycleAnalytics
class IssueStageEvent < ApplicationRecord
+ include StageEventModel
extend SuppressCompositePrimaryKeyWarning
validates(*%i[stage_event_hash_id issue_id group_id project_id start_event_timestamp], presence: true)
+
+ def self.issuable_id_column
+ :issue_id
+ end
end
end
end
diff --git a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
index d2f899ae93..d0ec3c4e8b 100644
--- a/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
+++ b/app/models/analytics/cycle_analytics/merge_request_stage_event.rb
@@ -3,9 +3,14 @@
module Analytics
module CycleAnalytics
class MergeRequestStageEvent < ApplicationRecord
+ include StageEventModel
extend SuppressCompositePrimaryKeyWarning
validates(*%i[stage_event_hash_id merge_request_id group_id project_id start_event_timestamp], presence: true)
+
+ def self.issuable_id_column
+ :merge_request_id
+ end
end
end
end
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 5f16b990d0..5a8cbd8d71 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -9,8 +9,6 @@ class ApplicationSetting < ApplicationRecord
include Sanitizable
ignore_columns %i[elasticsearch_shards elasticsearch_replicas], remove_with: '14.4', remove_after: '2021-09-22'
- ignore_column :seat_link_enabled, remove_with: '14.4', remove_after: '2021-09-22'
- ignore_column :cloud_license_enabled, remove_with: '14.4', remove_after: '2021-09-22'
INSTANCE_REVIEW_MIN_USERS = 50
GRAFANA_URL_ERROR_MESSAGE = 'Please check your Grafana URL setting in ' \
@@ -366,6 +364,10 @@ class ApplicationSetting < ApplicationRecord
validates :container_registry_expiration_policies_worker_capacity,
numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+ validates :dependency_proxy_ttl_group_policy_worker_capacity,
+ allow_nil: false,
+ numericality: { only_integer: true, greater_than_or_equal_to: 0 }
+
validates :invisible_captcha_enabled,
inclusion: { in: [true, false], message: _('must be a boolean value') }
@@ -481,6 +483,8 @@ class ApplicationSetting < ApplicationRecord
validates :throttle_unauthenticated_packages_api_period_in_seconds
validates :throttle_unauthenticated_files_api_requests_per_period
validates :throttle_unauthenticated_files_api_period_in_seconds
+ validates :throttle_unauthenticated_deprecated_api_requests_per_period
+ validates :throttle_unauthenticated_deprecated_api_period_in_seconds
validates :throttle_authenticated_api_requests_per_period
validates :throttle_authenticated_api_period_in_seconds
validates :throttle_authenticated_git_lfs_requests_per_period
@@ -491,6 +495,8 @@ class ApplicationSetting < ApplicationRecord
validates :throttle_authenticated_packages_api_period_in_seconds
validates :throttle_authenticated_files_api_requests_per_period
validates :throttle_authenticated_files_api_period_in_seconds
+ validates :throttle_authenticated_deprecated_api_requests_per_period
+ validates :throttle_authenticated_deprecated_api_period_in_seconds
validates :throttle_protected_paths_requests_per_period
validates :throttle_protected_paths_period_in_seconds
end
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 612fda158d..7bdea36bb8 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -159,6 +159,7 @@ module ApplicationSettingImplementation
spam_check_endpoint_enabled: false,
spam_check_endpoint_url: nil,
spam_check_api_key: nil,
+ suggest_pipeline_enabled: true,
terminal_max_session_time: 0,
throttle_authenticated_api_enabled: false,
throttle_authenticated_api_period_in_seconds: 3600,
@@ -175,6 +176,9 @@ module ApplicationSettingImplementation
throttle_authenticated_files_api_enabled: false,
throttle_authenticated_files_api_period_in_seconds: 15,
throttle_authenticated_files_api_requests_per_period: 500,
+ throttle_authenticated_deprecated_api_enabled: false,
+ throttle_authenticated_deprecated_api_period_in_seconds: 3600,
+ throttle_authenticated_deprecated_api_requests_per_period: 3600,
throttle_incident_management_notification_enabled: false,
throttle_incident_management_notification_per_period: 3600,
throttle_incident_management_notification_period_in_seconds: 3600,
@@ -193,6 +197,9 @@ module ApplicationSettingImplementation
throttle_unauthenticated_files_api_enabled: false,
throttle_unauthenticated_files_api_period_in_seconds: 15,
throttle_unauthenticated_files_api_requests_per_period: 125,
+ throttle_unauthenticated_deprecated_api_enabled: false,
+ throttle_unauthenticated_deprecated_api_period_in_seconds: 3600,
+ throttle_unauthenticated_deprecated_api_requests_per_period: 1800,
time_tracking_limit_to_hours: false,
two_factor_grace_period: 48,
unique_ips_limit_enabled: false,
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index f17fff742f..a1c6793607 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -71,7 +71,7 @@ class AuditEvent < ApplicationRecord
end
def lazy_author
- BatchLoader.for(author_id).batch(replace_methods: false) do |author_ids, loader|
+ BatchLoader.for(author_id).batch do |author_ids, loader|
User.select(:id, :name, :username).where(id: author_ids).find_each do |user|
loader.call(user.id, user)
end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index d251b0adbd..c8f6b9aaed 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -66,5 +66,3 @@ class AwardEmoji < ApplicationRecord
awardable.try(:update_upvotes_count) if upvote?
end
end
-
-AwardEmoji.prepend_mod_with('AwardEmoji')
diff --git a/app/models/bulk_import.rb b/app/models/bulk_import.rb
index dee5567530..818ae04ba2 100644
--- a/app/models/bulk_import.rb
+++ b/app/models/bulk_import.rb
@@ -4,7 +4,8 @@
# projects to a GitLab instance. It associates the import with the responsible
# user.
class BulkImport < ApplicationRecord
- MINIMUM_GITLAB_MAJOR_VERSION = 14
+ MIN_MAJOR_VERSION = 14
+ MIN_MINOR_VERSION_FOR_PROJECT = 4
belongs_to :user, optional: false
@@ -34,6 +35,14 @@ class BulkImport < ApplicationRecord
end
end
+ def source_version_info
+ Gitlab::VersionInfo.parse(source_version)
+ end
+
+ def self.min_gl_version_for_project_migration
+ Gitlab::VersionInfo.new(MIN_MAJOR_VERSION, MIN_MINOR_VERSION_FOR_PROJECT)
+ end
+
def self.all_human_statuses
state_machine.states.map(&:human_name)
end
diff --git a/app/models/bulk_imports/entity.rb b/app/models/bulk_imports/entity.rb
index ab5d248ff8..ecac4ab95f 100644
--- a/app/models/bulk_imports/entity.rb
+++ b/app/models/bulk_imports/entity.rb
@@ -20,6 +20,8 @@
class BulkImports::Entity < ApplicationRecord
self.table_name = 'bulk_import_entities'
+ EXPORT_RELATIONS_URL = '/%{resource}/%{full_path}/export_relations'
+
belongs_to :bulk_import, optional: false
belongs_to :parent, class_name: 'BulkImports::Entity', optional: true
@@ -81,9 +83,9 @@ class BulkImports::Entity < ApplicationRecord
def pipelines
@pipelines ||= case source_type
when 'group_entity'
- BulkImports::Groups::Stage.pipelines
+ BulkImports::Groups::Stage.new(bulk_import).pipelines
when 'project_entity'
- BulkImports::Projects::Stage.pipelines
+ BulkImports::Projects::Stage.new(bulk_import).pipelines
end
end
@@ -102,6 +104,14 @@ class BulkImports::Entity < ApplicationRecord
end
end
+ def pluralized_name
+ source_type.gsub('_entity', '').pluralize
+ end
+
+ def export_relations_url_path
+ @export_relations_url_path ||= EXPORT_RELATIONS_URL % { resource: pluralized_name, full_path: encoded_source_full_path }
+ end
+
private
def validate_parent_is_a_group
diff --git a/app/models/bulk_imports/export.rb b/app/models/bulk_imports/export.rb
index 371b58dea0..8d4d31ee92 100644
--- a/app/models/bulk_imports/export.rb
+++ b/app/models/bulk_imports/export.rb
@@ -53,7 +53,7 @@ module BulkImports
end
def relation_definition
- config.portable_tree[:include].find { |include| include[relation.to_sym] }
+ config.relation_definition_for(relation)
end
def config
diff --git a/app/models/bulk_imports/export_status.rb b/app/models/bulk_imports/export_status.rb
index ff165830cf..abf064adaa 100644
--- a/app/models/bulk_imports/export_status.rb
+++ b/app/models/bulk_imports/export_status.rb
@@ -41,7 +41,7 @@ module BulkImports
end
def status_endpoint
- "/groups/#{entity.encoded_source_full_path}/export_relations/status"
+ File.join(entity.export_relations_url_path, 'status')
end
end
end
diff --git a/app/models/bulk_imports/file_transfer/base_config.rb b/app/models/bulk_imports/file_transfer/base_config.rb
index ddea7c3f64..4d370315ad 100644
--- a/app/models/bulk_imports/file_transfer/base_config.rb
+++ b/app/models/bulk_imports/file_transfer/base_config.rb
@@ -22,15 +22,25 @@ module BulkImports
end
def export_path
- strong_memoize(:export_path) do
- relative_path = File.join(base_export_path, SecureRandom.hex)
-
- ::Gitlab::ImportExport.export_path(relative_path: relative_path)
- end
+ @export_path ||= Dir.mktmpdir('bulk_imports')
end
def portable_relations
- import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s) - skipped_relations
+ tree_relations + file_relations - skipped_relations
+ end
+
+ def tree_relation?(relation)
+ tree_relations.include?(relation)
+ end
+
+ def file_relation?(relation)
+ file_relations.include?(relation)
+ end
+
+ def tree_relation_definition_for(relation)
+ return unless tree_relation?(relation)
+
+ portable_tree[:include].find { |include| include[relation.to_sym] }
end
private
@@ -44,7 +54,7 @@ module BulkImports
end
def import_export_config
- ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h
+ @config ||= ::Gitlab::ImportExport::Config.new(config: import_export_yaml).to_h
end
def portable_class
@@ -63,8 +73,12 @@ module BulkImports
raise NotImplementedError
end
- def base_export_path
- raise NotImplementedError
+ def tree_relations
+ import_export_config.dig(:tree, portable_class_sym).keys.map(&:to_s)
+ end
+
+ def file_relations
+ []
end
def skipped_relations
diff --git a/app/models/bulk_imports/file_transfer/group_config.rb b/app/models/bulk_imports/file_transfer/group_config.rb
index 2266cbb484..6766c00246 100644
--- a/app/models/bulk_imports/file_transfer/group_config.rb
+++ b/app/models/bulk_imports/file_transfer/group_config.rb
@@ -3,16 +3,14 @@
module BulkImports
module FileTransfer
class GroupConfig < BaseConfig
- def base_export_path
- portable.full_path
- end
+ SKIPPED_RELATIONS = %w(members).freeze
def import_export_yaml
::Gitlab::ImportExport.group_config_file
end
def skipped_relations
- @skipped_relations ||= %w(members)
+ SKIPPED_RELATIONS
end
end
end
diff --git a/app/models/bulk_imports/file_transfer/project_config.rb b/app/models/bulk_imports/file_transfer/project_config.rb
index 8a57f51c1c..9a0434da08 100644
--- a/app/models/bulk_imports/file_transfer/project_config.rb
+++ b/app/models/bulk_imports/file_transfer/project_config.rb
@@ -3,16 +3,23 @@
module BulkImports
module FileTransfer
class ProjectConfig < BaseConfig
- def base_export_path
- portable.disk_path
- end
+ UPLOADS_RELATION = 'uploads'
+
+ SKIPPED_RELATIONS = %w(
+ project_members
+ group_members
+ ).freeze
def import_export_yaml
::Gitlab::ImportExport.config_file
end
+ def file_relations
+ [UPLOADS_RELATION]
+ end
+
def skipped_relations
- @skipped_relations ||= %w(project_members group_members)
+ SKIPPED_RELATIONS
end
end
end
diff --git a/app/models/bulk_imports/tracker.rb b/app/models/bulk_imports/tracker.rb
index c185470b1c..9de3239ee0 100644
--- a/app/models/bulk_imports/tracker.rb
+++ b/app/models/bulk_imports/tracker.rb
@@ -50,6 +50,8 @@ class BulkImports::Tracker < ApplicationRecord
event :start do
transition created: :started
+ # To avoid errors when re-starting a pipeline in case of network errors
+ transition started: :started
end
event :finish do
diff --git a/app/models/ci/bridge.rb b/app/models/ci/bridge.rb
index 97fb8233d3..50bda64d53 100644
--- a/app/models/ci/bridge.rb
+++ b/app/models/ci/bridge.rb
@@ -31,7 +31,7 @@ module Ci
next unless bridge.triggers_downstream_pipeline?
bridge.run_after_commit do
- ::Ci::CreateCrossProjectPipelineWorker.perform_async(bridge.id)
+ ::Ci::CreateDownstreamPipelineWorker.perform_async(bridge.id)
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index e2e2424767..990ef71a45 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -42,6 +42,10 @@ module Ci
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id, inverse_of: :build
has_many :report_results, class_name: 'Ci::BuildReportResult', inverse_of: :build
+ # Projects::DestroyService destroys Ci::Pipelines, which use_fast_destroy on :job_artifacts
+ # before we delete builds. By doing this, the relation should be empty and not fire any
+ # DELETE queries when the Ci::Build is destroyed. The next step is to remove `dependent: :destroy`.
+ # Details: https://gitlab.com/gitlab-org/gitlab/-/issues/24644#note_689472685
has_many :job_artifacts, class_name: 'Ci::JobArtifact', foreign_key: :job_id, dependent: :destroy, inverse_of: :job # rubocop:disable Cop/ActiveRecordDependent
has_many :job_variables, class_name: 'Ci::JobVariable', foreign_key: :job_id
has_many :sourced_pipelines, class_name: 'Ci::Sources::Pipeline', foreign_key: :source_job_id
@@ -55,6 +59,8 @@ module Ci
has_one :runner_session, class_name: 'Ci::BuildRunnerSession', validate: true, inverse_of: :build
has_one :trace_metadata, class_name: 'Ci::BuildTraceMetadata', inverse_of: :build
+ has_many :terraform_state_versions, class_name: 'Terraform::StateVersion', dependent: :nullify, inverse_of: :build, foreign_key: :ci_build_id # rubocop:disable Cop/ActiveRecordDependent
+
accepts_nested_attributes_for :runner_session, update_only: true
accepts_nested_attributes_for :job_variables
@@ -64,8 +70,8 @@ module Ci
delegate :gitlab_deploy_token, to: :project
delegate :trigger_short_token, to: :trigger_request, allow_nil: true
- ignore_columns :id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
- ignore_columns :stage_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
+ ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
+ ignore_columns :stage_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
##
# Since Gitlab 11.5, deployments records started being created right after
@@ -192,7 +198,6 @@ module Ci
add_authentication_token_field :token, encrypted: :required
before_save :ensure_token
- before_destroy { unscoped_project }
after_save :stick_build_if_status_changed
@@ -308,8 +313,10 @@ module Ci
end
after_transition pending: :running do |build|
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.run
+ unless build.update_deployment_after_transaction_commit?
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ build.deployment&.run
+ end
end
build.run_after_commit do
@@ -332,8 +339,10 @@ module Ci
end
after_transition any => [:success] do |build|
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment&.succeed
+ unless build.update_deployment_after_transaction_commit?
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ build.deployment&.succeed
+ end
end
build.run_after_commit do
@@ -346,12 +355,14 @@ module Ci
next unless build.project
next unless build.deployment
- begin
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- build.deployment.drop!
+ unless build.update_deployment_after_transaction_commit?
+ begin
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ build.deployment.drop!
+ end
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
end
- rescue StandardError => e
- Gitlab::ErrorTracking.track_and_raise_for_dev_exception(e, build_id: build.id)
end
true
@@ -370,14 +381,29 @@ module Ci
end
after_transition any => [:skipped, :canceled] do |build, transition|
- Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
- if transition.to_name == :skipped
- build.deployment&.skip
- else
- build.deployment&.cancel
+ unless build.update_deployment_after_transaction_commit?
+ Gitlab::Database.allow_cross_database_modification_within_transaction(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338867') do
+ if transition.to_name == :skipped
+ build.deployment&.skip
+ else
+ build.deployment&.cancel
+ end
end
end
end
+
+ # Synchronize Deployment Status
+ # Please note that the data integirty is not assured because we can't use
+ # a database transaction due to DB decomposition.
+ after_transition do |build, transition|
+ next if transition.loopback?
+ next unless build.project
+ next unless build.update_deployment_after_transaction_commit?
+
+ build.run_after_commit do
+ build.deployment&.sync_status_with(build)
+ end
+ end
end
def self.build_matchers(project)
@@ -1094,6 +1120,12 @@ module Ci
runner&.instance_type?
end
+ def update_deployment_after_transaction_commit?
+ strong_memoize(:update_deployment_after_transaction_commit) do
+ Feature.enabled?(:update_deployment_after_transaction_commit, project, default_enabled: :yaml)
+ end
+ end
+
protected
def run_status_commit_hooks!
@@ -1108,7 +1140,7 @@ module Ci
return unless saved_change_to_status?
return unless running?
- ::Gitlab::Database::LoadBalancing::Sticking.stick(:build, id)
+ self.class.sticking.stick(:build, id)
end
def status_commit_hooks
@@ -1154,10 +1186,6 @@ module Ci
self.update(erased_by: user, erased_at: Time.current, artifacts_expire_at: nil)
end
- def unscoped_project
- @unscoped_project ||= Project.unscoped.find_by(id: project_id)
- end
-
def environment_url
options&.dig(:environment, :url) || persisted_environment&.external_url
end
diff --git a/app/models/ci/build_metadata.rb b/app/models/ci/build_metadata.rb
index 90237a4be5..0d6d6f7a6a 100644
--- a/app/models/ci/build_metadata.rb
+++ b/app/models/ci/build_metadata.rb
@@ -37,8 +37,8 @@ module Ci
job_timeout_source: 4
}
- ignore_column :build_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
- ignore_columns :id_convert_to_bigint, remove_with: '14.3', remove_after: '2021-09-22'
+ ignore_column :build_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
+ ignore_columns :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
def update_timeout_state
timeout = timeout_with_highest_precedence
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
index 003659570b..bf1470ca20 100644
--- a/app/models/ci/build_need.rb
+++ b/app/models/ci/build_need.rb
@@ -5,8 +5,6 @@ module Ci
include BulkInsertSafe
include IgnorableColumns
- ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
-
belongs_to :build, class_name: "Ci::Processable", foreign_key: :build_id, inverse_of: :needs
validates :build, presence: true
diff --git a/app/models/ci/build_runner_session.rb b/app/models/ci/build_runner_session.rb
index 45de47116c..e12c0f82c9 100644
--- a/app/models/ci/build_runner_session.rb
+++ b/app/models/ci/build_runner_session.rb
@@ -6,8 +6,6 @@ module Ci
class BuildRunnerSession < Ci::ApplicationRecord
include IgnorableColumns
- ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
-
TERMINAL_SUBPROTOCOL = 'terminal.gitlab.com'
DEFAULT_SERVICE_NAME = 'build'
DEFAULT_PORT_NAME = 'default_port'
diff --git a/app/models/ci/build_trace_chunk.rb b/app/models/ci/build_trace_chunk.rb
index 7a15d7ba94..6edb5ef457 100644
--- a/app/models/ci/build_trace_chunk.rb
+++ b/app/models/ci/build_trace_chunk.rb
@@ -9,8 +9,6 @@ module Ci
include ::Gitlab::OptimisticLocking
include IgnorableColumns
- ignore_columns :build_id_convert_to_bigint, remove_with: '14.1', remove_after: '2021-07-22'
-
belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id
default_value_for :data_store, :redis_trace_chunks
diff --git a/app/models/ci/build_trace_metadata.rb b/app/models/ci/build_trace_metadata.rb
index 901b84ceec..1ffa0e31f9 100644
--- a/app/models/ci/build_trace_metadata.rb
+++ b/app/models/ci/build_trace_metadata.rb
@@ -37,8 +37,10 @@ module Ci
increment!(:archival_attempts, touch: :last_archival_attempt_at)
end
- def track_archival!(trace_artifact_id)
- update!(trace_artifact_id: trace_artifact_id, archived_at: Time.current)
+ def track_archival!(trace_artifact_id, checksum)
+ update!(trace_artifact_id: trace_artifact_id,
+ checksum: checksum,
+ archived_at: Time.current)
end
def archival_attempts_message
@@ -49,6 +51,11 @@ module Ci
end
end
+ def remote_checksum_valid?
+ checksum.present? &&
+ checksum == remote_checksum
+ end
+
private
def backoff
diff --git a/app/models/ci/job_token/project_scope_link.rb b/app/models/ci/job_token/project_scope_link.rb
index 99118f8090..c2ab8ca092 100644
--- a/app/models/ci/job_token/project_scope_link.rb
+++ b/app/models/ci/job_token/project_scope_link.rb
@@ -5,7 +5,7 @@
module Ci
module JobToken
- class ProjectScopeLink < ApplicationRecord
+ class ProjectScopeLink < Ci::ApplicationRecord
self.table_name = 'ci_job_token_project_scope_links'
belongs_to :source_project, class_name: 'Project'
diff --git a/app/models/ci/job_token/scope.rb b/app/models/ci/job_token/scope.rb
index 42cfdc21d6..3a5765aa00 100644
--- a/app/models/ci/job_token/scope.rb
+++ b/app/models/ci/job_token/scope.rb
@@ -32,12 +32,15 @@ module Ci
def all_projects
Project.from_union([
Project.id_in(source_project),
- Project.where_exists(
- Ci::JobToken::ProjectScopeLink
- .from_project(source_project)
- .where('projects.id = ci_job_token_project_scope_links.target_project_id'))
+ Project.id_in(target_project_ids)
], remove_duplicates: false)
end
+
+ private
+
+ def target_project_ids
+ Ci::JobToken::ProjectScopeLink.from_project(source_project).pluck(:target_project_id)
+ end
end
end
end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1a0cec3c93..0041ec5135 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -82,7 +82,8 @@ module Ci
# Merge requests for which the current pipeline is running against
# the merge request's latest commit.
has_many :merge_requests_as_head_pipeline, foreign_key: "head_pipeline_id", class_name: 'MergeRequest'
-
+ has_many :package_build_infos, class_name: 'Packages::BuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent
+ has_many :package_file_build_infos, class_name: 'Packages::PackageFileBuildInfo', dependent: :nullify, inverse_of: :pipeline # rubocop:disable Cop/ActiveRecordDependent
has_many :pending_builds, -> { pending }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :failed_builds, -> { latest.failed }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
has_many :retryable_builds, -> { latest.failed_or_canceled.includes(:project) }, foreign_key: :commit_id, class_name: 'Ci::Build', inverse_of: :pipeline
@@ -861,11 +862,6 @@ module Ci
self.duration = Gitlab::Ci::Pipeline::Duration.from_pipeline(self)
end
- def execute_hooks
- project.execute_hooks(pipeline_data, :pipeline_hooks) if project.has_active_hooks?(:pipeline_hooks)
- project.execute_integrations(pipeline_data, :pipeline_hooks) if project.has_active_integrations?(:pipeline_hooks)
- end
-
# All the merge requests for which the current pipeline runs/ran against
def all_merge_requests
@all_merge_requests ||=
@@ -929,9 +925,22 @@ module Ci
end
def environments_in_self_and_descendants
- environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id')
+ if ::Feature.enabled?(:avoid_cross_joins_environments_in_self_and_descendants, default_enabled: :yaml)
+ # We limit to 100 unique environments for application safety.
+ # See: https://gitlab.com/gitlab-org/gitlab/-/issues/340781#note_699114700
+ expanded_environment_names =
+ builds_in_self_and_descendants.joins(:metadata)
+ .where.not('ci_builds_metadata.expanded_environment_name' => nil)
+ .distinct('ci_builds_metadata.expanded_environment_name')
+ .limit(100)
+ .pluck(:expanded_environment_name)
- Environment.where(id: environment_ids)
+ Environment.where(project: project, name: expanded_environment_names)
+ else
+ environment_ids = self_and_descendants.joins(:deployments).select(:'deployments.environment_id')
+
+ Environment.where(id: environment_ids)
+ end
end
# With multi-project and parent-child pipelines
@@ -1251,12 +1260,6 @@ module Ci
messages.build(severity: severity, content: content)
end
- def pipeline_data
- strong_memoize(:pipeline_data) do
- Gitlab::DataBuilder::Pipeline.build(self)
- end
- end
-
def merge_request_diff_sha
return unless merge_request?
diff --git a/app/models/ci/processable.rb b/app/models/ci/processable.rb
index 30d335fd7d..372df8cc26 100644
--- a/app/models/ci/processable.rb
+++ b/app/models/ci/processable.rb
@@ -58,7 +58,8 @@ module Ci
after_transition any => ::Ci::Processable.completed_statuses do |processable|
next unless processable.with_resource_group?
- next unless processable.resource_group.release_resource_from(processable)
+
+ processable.resource_group.release_resource_from(processable)
processable.run_after_commit do
Ci::ResourceGroups::AssignResourceFromResourceGroupWorker
diff --git a/app/models/ci/resource_group.rb b/app/models/ci/resource_group.rb
index 8a7456041e..6d25f747a9 100644
--- a/app/models/ci/resource_group.rb
+++ b/app/models/ci/resource_group.rb
@@ -14,6 +14,12 @@ module Ci
before_create :ensure_resource
+ enum process_mode: {
+ unordered: 0,
+ oldest_first: 1,
+ newest_first: 2
+ }
+
##
# NOTE: This is concurrency-safe method that the subquery in the `UPDATE`
# works as explicit locking.
@@ -25,8 +31,34 @@ module Ci
resources.retained_by(processable).update_all(build_id: nil) > 0
end
+ def upcoming_processables
+ if unordered?
+ processables.waiting_for_resource
+ elsif oldest_first?
+ processables.waiting_for_resource_or_upcoming
+ .order(Arel.sql("commit_id ASC, #{sort_by_job_status}"))
+ elsif newest_first?
+ processables.waiting_for_resource_or_upcoming
+ .order(Arel.sql("commit_id DESC, #{sort_by_job_status}"))
+ else
+ Ci::Processable.none
+ end
+ end
+
private
+ # In order to avoid deadlock, we do NOT specify the job execution order in the same pipeline.
+ # The system processes wherever ready to transition to `pending` status from `waiting_for_resource`.
+ # See https://gitlab.com/gitlab-org/gitlab/-/issues/202186 for more information.
+ def sort_by_job_status
+ <<~SQL
+ CASE status
+ WHEN 'waiting_for_resource' THEN 0
+ ELSE 1
+ END ASC
+ SQL
+ end
+
def ensure_resource
# Currently we only support one resource per group, which means
# maximum one build can be set to the resource group, thus builds
diff --git a/app/models/ci/runner.rb b/app/models/ci/runner.rb
index 4aa232ad26..2f718ad758 100644
--- a/app/models/ci/runner.rb
+++ b/app/models/ci/runner.rb
@@ -51,7 +51,7 @@ module Ci
has_many :runner_projects, inverse_of: :runner, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :projects, through: :runner_projects
has_many :runner_namespaces, inverse_of: :runner, autosave: true
- has_many :groups, through: :runner_namespaces
+ has_many :groups, through: :runner_namespaces, disable_joins: true
has_one :last_build, -> { order('id DESC') }, class_name: 'Ci::Build'
@@ -246,7 +246,7 @@ module Ci
begin
transaction do
- self.projects << project
+ self.runner_projects << ::Ci::RunnerProject.new(project: project, runner: self)
self.save!
end
rescue ActiveRecord::RecordInvalid => e
@@ -280,7 +280,7 @@ module Ci
end
def belongs_to_more_than_one_project?
- self.projects.limit(2).count(:all) > 1
+ runner_projects.limit(2).count(:all) > 1
end
def assigned_to_group?
@@ -309,7 +309,9 @@ module Ci
end
def only_for?(project)
- projects == [project]
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
+ projects == [project]
+ end
end
def short_sha
@@ -344,7 +346,7 @@ module Ci
# intention here is not to execute `Ci::RegisterJobService#execute` on
# the primary database.
#
- ::Gitlab::Database::LoadBalancing::Sticking.stick(:runner, id)
+ ::Ci::Runner.sticking.stick(:runner, id)
SecureRandom.hex.tap do |new_update|
::Gitlab::Workhorse.set_key_and_notify(runner_queue_key, new_update,
@@ -428,10 +430,8 @@ module Ci
end
def no_projects
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
- if projects.any?
- errors.add(:runner, 'cannot have projects assigned')
- end
+ if runner_projects.any?
+ errors.add(:runner, 'cannot have projects assigned')
end
end
@@ -444,14 +444,16 @@ module Ci
end
def any_project
- unless projects.any?
+ unless runner_projects.any?
errors.add(:runner, 'needs to be assigned to at least one project')
end
end
def exactly_one_group
- unless groups.one?
- errors.add(:runner, 'needs to be assigned to exactly one group')
+ ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/338659') do
+ unless groups.one?
+ errors.add(:runner, 'needs to be assigned to exactly one group')
+ end
end
end
diff --git a/app/models/ci/runner_namespace.rb b/app/models/ci/runner_namespace.rb
index d1353b97ed..52a31863fb 100644
--- a/app/models/ci/runner_namespace.rb
+++ b/app/models/ci/runner_namespace.rb
@@ -7,7 +7,6 @@ module Ci
self.limit_name = 'ci_registered_group_runners'
self.limit_scope = :group
self.limit_relation = :recent_runners
- self.limit_feature_flag = :ci_runner_limits
self.limit_feature_flag_for_override = :ci_runner_limits_override
belongs_to :runner, inverse_of: :runner_namespaces
diff --git a/app/models/ci/runner_project.rb b/app/models/ci/runner_project.rb
index e1c435e9b1..148a29a0f8 100644
--- a/app/models/ci/runner_project.rb
+++ b/app/models/ci/runner_project.rb
@@ -7,7 +7,6 @@ module Ci
self.limit_name = 'ci_registered_project_runners'
self.limit_scope = :project
self.limit_relation = :recent_runners
- self.limit_feature_flag = :ci_runner_limits
self.limit_feature_flag_for_override = :ci_runner_limits_override
belongs_to :runner, inverse_of: :runner_projects
diff --git a/app/models/ci/stage.rb b/app/models/ci/stage.rb
index 39e26bf278..131e18adf6 100644
--- a/app/models/ci/stage.rb
+++ b/app/models/ci/stage.rb
@@ -8,8 +8,6 @@ module Ci
include Presentable
include IgnorableColumns
- ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
-
enum status: Ci::HasStatus::STATUSES_ENUM
belongs_to :project
diff --git a/app/models/clusters/agents/group_authorization.rb b/app/models/clusters/agents/group_authorization.rb
index 74c0cec3b7..28a711aaf1 100644
--- a/app/models/clusters/agents/group_authorization.rb
+++ b/app/models/clusters/agents/group_authorization.rb
@@ -10,7 +10,9 @@ module Clusters
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
- delegate :project, to: :agent
+ def config_project
+ agent.project
+ end
end
end
end
diff --git a/app/models/clusters/agents/implicit_authorization.rb b/app/models/clusters/agents/implicit_authorization.rb
index 967cc68604..9f7f653ed6 100644
--- a/app/models/clusters/agents/implicit_authorization.rb
+++ b/app/models/clusters/agents/implicit_authorization.rb
@@ -6,12 +6,15 @@ module Clusters
attr_reader :agent
delegate :id, to: :agent, prefix: true
- delegate :project, to: :agent
def initialize(agent:)
@agent = agent
end
+ def config_project
+ agent.project
+ end
+
def config
nil
end
diff --git a/app/models/clusters/agents/project_authorization.rb b/app/models/clusters/agents/project_authorization.rb
index 1c71a0a432..f6d1908675 100644
--- a/app/models/clusters/agents/project_authorization.rb
+++ b/app/models/clusters/agents/project_authorization.rb
@@ -9,6 +9,10 @@ module Clusters
belongs_to :project, class_name: '::Project', optional: false
validates :config, json_schema: { filename: 'cluster_agent_authorization_configuration' }
+
+ def config_project
+ agent.project
+ end
end
end
end
diff --git a/app/models/clusters/applications/runner.rb b/app/models/clusters/applications/runner.rb
index 993ccb3365..7cef92ce81 100644
--- a/app/models/clusters/applications/runner.rb
+++ b/app/models/clusters/applications/runner.rb
@@ -72,7 +72,7 @@ module Clusters
if cluster.group_type?
attributes[:groups] = [group]
elsif cluster.project_type?
- attributes[:projects] = [project]
+ attributes[:runner_projects] = [::Ci::RunnerProject.new(project: project)]
end
attributes
diff --git a/app/models/clusters/integrations/elastic_stack.rb b/app/models/clusters/integrations/elastic_stack.rb
index 565d268259..97d73d252b 100644
--- a/app/models/clusters/integrations/elastic_stack.rb
+++ b/app/models/clusters/integrations/elastic_stack.rb
@@ -14,6 +14,8 @@ module Clusters
validates :cluster, presence: true
validates :enabled, inclusion: { in: [true, false] }
+ scope :enabled, -> { where(enabled: true) }
+
def available?
enabled
end
diff --git a/app/models/clusters/integrations/prometheus.rb b/app/models/clusters/integrations/prometheus.rb
index 3f2c47d48e..d745a49afc 100644
--- a/app/models/clusters/integrations/prometheus.rb
+++ b/app/models/clusters/integrations/prometheus.rb
@@ -21,6 +21,8 @@ module Clusters
default_value_for(:alert_manager_token) { SecureRandom.hex }
+ scope :enabled, -> { where(enabled: true) }
+
after_destroy do
run_after_commit do
deactivate_project_integrations
diff --git a/app/models/commit.rb b/app/models/commit.rb
index 6c8b4ae113..553681ee96 100644
--- a/app/models/commit.rb
+++ b/app/models/commit.rb
@@ -133,7 +133,7 @@ class Commit
end
def lazy(container, oid)
- BatchLoader.for({ container: container, oid: oid }).batch(replace_methods: false) do |items, loader|
+ BatchLoader.for({ container: container, oid: oid }).batch do |items, loader|
items_by_container = items.group_by { |i| i[:container] }
items_by_container.each do |container, commit_ids|
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index 8cba3d0450..43427e2ebc 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -62,6 +62,9 @@ class CommitStatus < Ci::ApplicationRecord
scope :updated_before, ->(lookback:, timeout:) {
where('(ci_builds.created_at BETWEEN ? AND ?) AND (ci_builds.updated_at BETWEEN ? AND ?)', lookback, timeout, lookback, timeout)
}
+ scope :scheduled_at_before, ->(date) {
+ where('ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?', date)
+ }
# The scope applies `pluck` to split the queries. Use with care.
scope :for_project_paths, -> (paths) do
diff --git a/app/models/concerns/analytics/cycle_analytics/stage.rb b/app/models/concerns/analytics/cycle_analytics/stage.rb
index 7bb6004ca8..d9e6756ab8 100644
--- a/app/models/concerns/analytics/cycle_analytics/stage.rb
+++ b/app/models/concerns/analytics/cycle_analytics/stage.rb
@@ -27,7 +27,8 @@ module Analytics
alias_attribute :custom_stage?, :custom
scope :default_stages, -> { where(custom: false) }
scope :ordered, -> { order(:relative_position, :id) }
- scope :for_list, -> { includes(:start_event_label, :end_event_label).ordered }
+ scope :with_preloaded_labels, -> { includes(:start_event_label, :end_event_label) }
+ scope :for_list, -> { with_preloaded_labels.ordered }
scope :by_value_stream, -> (value_stream) { where(value_stream_id: value_stream.id) }
before_save :ensure_stage_event_hash_id
diff --git a/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
new file mode 100644
index 0000000000..7462e1e828
--- /dev/null
+++ b/app/models/concerns/analytics/cycle_analytics/stage_event_model.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+
+module Analytics
+ module CycleAnalytics
+ module StageEventModel
+ extend ActiveSupport::Concern
+
+ class_methods do
+ def upsert_data(data)
+ upsert_values = data.map do |row|
+ row.values_at(
+ :stage_event_hash_id,
+ :issuable_id,
+ :group_id,
+ :project_id,
+ :author_id,
+ :milestone_id,
+ :start_event_timestamp,
+ :end_event_timestamp
+ )
+ end
+
+ value_list = Arel::Nodes::ValuesList.new(upsert_values).to_sql
+
+ query = <<~SQL
+ INSERT INTO #{quoted_table_name}
+ (
+ stage_event_hash_id,
+ #{connection.quote_column_name(issuable_id_column)},
+ group_id,
+ project_id,
+ milestone_id,
+ author_id,
+ start_event_timestamp,
+ end_event_timestamp
+ )
+ #{value_list}
+ ON CONFLICT(stage_event_hash_id, #{issuable_id_column})
+ DO UPDATE SET
+ group_id = excluded.group_id,
+ project_id = excluded.project_id,
+ start_event_timestamp = excluded.start_event_timestamp,
+ end_event_timestamp = excluded.end_event_timestamp,
+ milestone_id = excluded.milestone_id,
+ author_id = excluded.author_id
+ SQL
+
+ result = connection.execute(query)
+ result.cmd_tuples
+ end
+ end
+ end
+ end
+end
diff --git a/app/models/concerns/avatarable.rb b/app/models/concerns/avatarable.rb
index 84a74386ff..b32502c3ee 100644
--- a/app/models/concerns/avatarable.rb
+++ b/app/models/concerns/avatarable.rb
@@ -18,6 +18,7 @@ module Avatarable
prepend ShadowMethods
include ObjectStorage::BackgroundMove
include Gitlab::Utils::StrongMemoize
+ include ApplicationHelper
validate :avatar_type, if: ->(user) { user.avatar.present? && user.avatar_changed? }
validates :avatar, file_size: { maximum: MAXIMUM_FILE_SIZE }, if: :avatar_changed?
@@ -110,7 +111,7 @@ module Avatarable
def retrieve_upload_from_batch(identifier)
BatchLoader.for(identifier: identifier, model: self)
- .batch(key: self.class, cache: true, replace_methods: false) do |upload_params, loader, args|
+ .batch(key: self.class) do |upload_params, loader, args|
model_class = args[:key]
paths = upload_params.flat_map do |params|
params[:model].upload_paths(params[:identifier])
diff --git a/app/models/concerns/bulk_insert_safe.rb b/app/models/concerns/bulk_insert_safe.rb
index 908f0b6a7e..6c3093ca91 100644
--- a/app/models/concerns/bulk_insert_safe.rb
+++ b/app/models/concerns/bulk_insert_safe.rb
@@ -51,6 +51,12 @@ module BulkInsertSafe
PrimaryKeySetError = Class.new(StandardError)
class_methods do
+ def insert_all_proxy_class
+ @insert_all_proxy_class ||= Class.new(self) do
+ attr_readonly :created_at
+ end
+ end
+
def set_callback(name, *args)
unless _bulk_insert_callback_allowed?(name, args)
raise MethodNotAllowedError,
@@ -138,7 +144,7 @@ module BulkInsertSafe
when nil
false
else
- raise ArgumentError, "returns needs to be :ids or nil"
+ returns
end
# Handle insertions for tables with a composite primary key
@@ -153,9 +159,9 @@ module BulkInsertSafe
item_batch, validate, &handle_attributes)
ActiveRecord::InsertAll
- .new(self, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by)
+ .new(insert_all_proxy_class, attributes, on_duplicate: on_duplicate, returning: returning, unique_by: unique_by)
.execute
- .pluck(primary_key)
+ .cast_values(insert_all_proxy_class.attribute_types).to_a
end
end
end
diff --git a/app/models/concerns/checksummable.rb b/app/models/concerns/checksummable.rb
index 056abafd0c..9812c62fcc 100644
--- a/app/models/concerns/checksummable.rb
+++ b/app/models/concerns/checksummable.rb
@@ -8,8 +8,12 @@ module Checksummable
Zlib.crc32(data)
end
- def hexdigest(path)
+ def sha256_hexdigest(path)
::Digest::SHA256.file(path).hexdigest
end
+
+ def md5_hexdigest(path)
+ ::Digest::MD5.file(path).hexdigest
+ end
end
end
diff --git a/app/models/concerns/ci/has_status.rb b/app/models/concerns/ci/has_status.rb
index c1299e3d46..8d715279da 100644
--- a/app/models/concerns/ci/has_status.rb
+++ b/app/models/concerns/ci/has_status.rb
@@ -95,6 +95,7 @@ module Ci
scope :failed_or_canceled, -> { with_status(:failed, :canceled) }
scope :complete, -> { with_status(completed_statuses) }
scope :incomplete, -> { without_statuses(completed_statuses) }
+ scope :waiting_for_resource_or_upcoming, -> { with_status(:created, :scheduled, :waiting_for_resource) }
scope :cancelable, -> do
where(status: [:running, :waiting_for_resource, :preparing, :pending, :created, :scheduled])
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index ec86746ae5..344f5aa4cd 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -20,6 +20,7 @@ module Ci
delegate :interruptible, to: :metadata, prefix: false, allow_nil: true
delegate :has_exposed_artifacts?, to: :metadata, prefix: false, allow_nil: true
delegate :environment_auto_stop_in, to: :metadata, prefix: false, allow_nil: true
+ delegate :runner_features, to: :metadata, prefix: false, allow_nil: false
before_create :ensure_metadata
end
diff --git a/app/models/concerns/enums/ci/commit_status.rb b/app/models/concerns/enums/ci/commit_status.rb
index 7f46e44697..1b4cc14f4a 100644
--- a/app/models/concerns/enums/ci/commit_status.rb
+++ b/app/models/concerns/enums/ci/commit_status.rb
@@ -27,6 +27,7 @@ module Enums
no_matching_runner: 18, # not used anymore, but cannot be deleted because of old data
trace_size_exceeded: 19,
builds_disabled: 20,
+ environment_creation_failure: 21,
insufficient_bridge_permissions: 1_001,
downstream_bridge_project_not_found: 1_002,
invalid_bridge_trigger: 1_003,
diff --git a/app/models/concerns/has_repository.rb b/app/models/concerns/has_repository.rb
index 9218ba47d2..d614d6c458 100644
--- a/app/models/concerns/has_repository.rb
+++ b/app/models/concerns/has_repository.rb
@@ -72,12 +72,10 @@ module HasRepository
end
def default_branch
- @default_branch ||= repository.root_ref || default_branch_from_preferences
+ @default_branch ||= repository.empty? ? default_branch_from_preferences : repository.root_ref
end
def default_branch_from_preferences
- return unless empty_repo?
-
(default_branch_from_group_preferences || Gitlab::CurrentSettings.default_branch_name).presence
end
diff --git a/app/models/concerns/integrations/has_data_fields.rb b/app/models/concerns/integrations/has_data_fields.rb
index 1709b56080..25a1d85511 100644
--- a/app/models/concerns/integrations/has_data_fields.rb
+++ b/app/models/concerns/integrations/has_data_fields.rb
@@ -45,7 +45,6 @@ module Integrations
included do
has_one :issue_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::IssueTrackerData'
has_one :jira_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::JiraTrackerData'
- has_one :open_project_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :service_id, class_name: 'Integrations::OpenProjectTrackerData'
has_one :zentao_tracker_data, autosave: true, inverse_of: :integration, foreign_key: :integration_id, class_name: 'Integrations::ZentaoTrackerData'
def data_fields
diff --git a/app/models/concerns/issue_available_features.rb b/app/models/concerns/issue_available_features.rb
index 933e8b5f68..209456f8b6 100644
--- a/app/models/concerns/issue_available_features.rb
+++ b/app/models/concerns/issue_available_features.rb
@@ -12,7 +12,8 @@ module IssueAvailableFeatures
{
assignee: %w(issue incident),
confidentiality: %w(issue incident),
- time_tracking: %w(issue incident)
+ time_tracking: %w(issue incident),
+ move_and_clone: %w(issue incident)
}.with_indifferent_access
end
end
diff --git a/app/models/concerns/packages/debian/distribution.rb b/app/models/concerns/packages/debian/distribution.rb
index 196bec04be..ff52769fce 100644
--- a/app/models/concerns/packages/debian/distribution.rb
+++ b/app/models/concerns/packages/debian/distribution.rb
@@ -96,18 +96,8 @@ module Packages
architectures.pluck(:name).sort
end
- def needs_update?
- !file.exists? || time_duration_expired?
- end
-
private
- def time_duration_expired?
- return false unless valid_time_duration_seconds.present?
-
- updated_at + valid_time_duration_seconds.seconds + 6.hours < Time.current
- end
-
def unique_codename_and_suite
errors.add(:codename, _('has already been taken as Suite')) if codename_exists_as_suite?
errors.add(:suite, _('has already been taken as Codename')) if suite_exists_as_codename?
diff --git a/app/models/concerns/restricted_signup.rb b/app/models/concerns/restricted_signup.rb
index 587f8c35ff..cf97be2116 100644
--- a/app/models/concerns/restricted_signup.rb
+++ b/app/models/concerns/restricted_signup.rb
@@ -7,15 +7,49 @@ module RestrictedSignup
def validate_admin_signup_restrictions(email)
return if allowed_domain?(email)
- if allowlist_present?
- return _('domain is not authorized for sign-up.')
- elsif denied_domain?(email)
- return _('is not from an allowed domain.')
- elsif restricted_email?(email)
- return _('is not allowed. Try again with a different email address, or contact your GitLab admin.')
- end
+ error_type = fetch_error_type(email)
- nil
+ return unless error_type.present?
+
+ [
+ signup_email_invalid_message,
+ error_message[created_by_key][error_type]
+ ].join(' ')
+ end
+
+ def fetch_error_type(email)
+ if allowlist_present?
+ :allowlist
+ elsif denied_domain?(email)
+ :denylist
+ elsif restricted_email?(email)
+ :restricted
+ end
+ end
+
+ def error_message
+ {
+ admin: {
+ allowlist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Allowed domains for sign-ups'.")).html_safe,
+ denylist: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check the 'Domain denylist'.")).html_safe,
+ restricted: html_escape_once(_("Go to the 'Admin area > Sign-up restrictions', and check 'Email restrictions for sign-ups'.")).html_safe,
+ group_setting: html_escape_once(_("Go to the group’s 'Settings > General' page, and check 'Restrict membership by email domain'.")).html_safe
+ },
+ nonadmin: {
+ allowlist: error_nonadmin,
+ denylist: error_nonadmin,
+ restricted: error_nonadmin,
+ group_setting: error_nonadmin
+ }
+ }
+ end
+
+ def error_nonadmin
+ _("Check with your administrator.")
+ end
+
+ def created_by_key
+ created_by&.can_admin_all_resources? ? :admin : :nonadmin
end
def denied_domain?(email)
diff --git a/app/models/concerns/routable.rb b/app/models/concerns/routable.rb
index 847abdc1b6..f382b3624e 100644
--- a/app/models/concerns/routable.rb
+++ b/app/models/concerns/routable.rb
@@ -41,7 +41,7 @@ module Routable
has_one :route, as: :source, autosave: true, dependent: :destroy, inverse_of: :source # rubocop:disable Cop/ActiveRecordDependent
has_many :redirect_routes, as: :source, autosave: true, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- validates :route, presence: true
+ validates :route, presence: true, unless: -> { is_a?(Namespaces::ProjectNamespace) }
scope :with_route, -> { includes(:route) }
@@ -185,6 +185,7 @@ module Routable
def prepare_route
return unless full_path_changed? || full_name_changed?
+ return if is_a?(Namespaces::ProjectNamespace)
route || build_route(source: self)
route.path = build_full_path
diff --git a/app/models/concerns/ttl_expirable.rb b/app/models/concerns/ttl_expirable.rb
new file mode 100644
index 0000000000..00abe0a06e
--- /dev/null
+++ b/app/models/concerns/ttl_expirable.rb
@@ -0,0 +1,20 @@
+# frozen_string_literal: true
+
+module TtlExpirable
+ extend ActiveSupport::Concern
+
+ included do
+ validates :status, presence: true
+
+ enum status: { default: 0, expired: 1, processing: 2, error: 3 }
+
+ scope :updated_before, ->(number_of_days) { where("updated_at <= ?", Time.zone.now - number_of_days.days) }
+ scope :active, -> { where(status: :default) }
+
+ scope :lock_next_by, ->(sort) do
+ order(sort)
+ .limit(1)
+ .lock('FOR UPDATE SKIP LOCKED')
+ end
+ end
+end
diff --git a/app/models/concerns/vulnerability_finding_helpers.rb b/app/models/concerns/vulnerability_finding_helpers.rb
index a656856487..7f96b3901f 100644
--- a/app/models/concerns/vulnerability_finding_helpers.rb
+++ b/app/models/concerns/vulnerability_finding_helpers.rb
@@ -2,6 +2,15 @@
module VulnerabilityFindingHelpers
extend ActiveSupport::Concern
+
+ # Manually resolvable report types cannot be considered fixed once removed from the
+ # target branch due to requiring active triage, such as rotation of an exposed token.
+ REPORT_TYPES_REQUIRING_MANUAL_RESOLUTION = %w[secret_detection].freeze
+
+ def requires_manual_resolution?
+ REPORT_TYPES_REQUIRING_MANUAL_RESOLUTION.include?(report_type)
+ end
+
def matches_signatures(other_signatures, other_uuid)
other_signature_types = other_signatures.index_by(&:algorithm_type)
diff --git a/app/models/container_expiration_policy.rb b/app/models/container_expiration_policy.rb
index 9bacd9a0ed..aecb47f7a0 100644
--- a/app/models/container_expiration_policy.rb
+++ b/app/models/container_expiration_policy.rb
@@ -74,6 +74,7 @@ class ContainerExpirationPolicy < ApplicationRecord
'7d': _('%{days} days until tags are automatically removed') % { days: 7 },
'14d': _('%{days} days until tags are automatically removed') % { days: 14 },
'30d': _('%{days} days until tags are automatically removed') % { days: 30 },
+ '60d': _('%{days} days until tags are automatically removed') % { days: 60 },
'90d': _('%{days} days until tags are automatically removed') % { days: 90 }
}
end
diff --git a/app/models/custom_emoji.rb b/app/models/custom_emoji.rb
index aea48a5ec2..ecdac64b31 100644
--- a/app/models/custom_emoji.rb
+++ b/app/models/custom_emoji.rb
@@ -5,7 +5,7 @@ class CustomEmoji < ApplicationRecord
belongs_to :namespace, inverse_of: :custom_emoji
- belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
+ belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id'
belongs_to :creator, class_name: "User", inverse_of: :created_custom_emoji
# For now only external emoji are supported. See https://gitlab.com/gitlab-org/gitlab/-/issues/230467
diff --git a/app/models/customer_relations/contact.rb b/app/models/customer_relations/contact.rb
index aaa7e2ae17..c632f8e2ef 100644
--- a/app/models/customer_relations/contact.rb
+++ b/app/models/customer_relations/contact.rb
@@ -5,8 +5,9 @@ class CustomerRelations::Contact < ApplicationRecord
self.table_name = "customer_relations_contacts"
- belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
+ belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id'
belongs_to :organization, optional: true
+ has_and_belongs_to_many :issues, join_table: :issue_customer_relations_contacts # rubocop: disable Rails/HasAndBelongsToMany
strip_attributes! :phone, :first_name, :last_name
diff --git a/app/models/customer_relations/organization.rb b/app/models/customer_relations/organization.rb
index a18d3ab814..c206d1e05f 100644
--- a/app/models/customer_relations/organization.rb
+++ b/app/models/customer_relations/organization.rb
@@ -5,7 +5,7 @@ class CustomerRelations::Organization < ApplicationRecord
self.table_name = "customer_relations_organizations"
- belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'group_id'
+ belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'group_id'
strip_attributes! :name
diff --git a/app/models/dependency_proxy/blob.rb b/app/models/dependency_proxy/blob.rb
index 5de6b1cf28..7ca1565258 100644
--- a/app/models/dependency_proxy/blob.rb
+++ b/app/models/dependency_proxy/blob.rb
@@ -2,15 +2,14 @@
class DependencyProxy::Blob < ApplicationRecord
include FileStoreMounter
+ include TtlExpirable
+ include EachBatch
belongs_to :group
validates :group, presence: true
validates :file, presence: true
validates :file_name, presence: true
- validates :status, presence: true
-
- enum status: { default: 0, expired: 1 }
mount_file_store_uploader DependencyProxy::FileUploader
diff --git a/app/models/dependency_proxy/image_ttl_group_policy.rb b/app/models/dependency_proxy/image_ttl_group_policy.rb
index 5a1b8cb8f1..0dfb298a39 100644
--- a/app/models/dependency_proxy/image_ttl_group_policy.rb
+++ b/app/models/dependency_proxy/image_ttl_group_policy.rb
@@ -8,4 +8,6 @@ class DependencyProxy::ImageTtlGroupPolicy < ApplicationRecord
validates :group, presence: true
validates :enabled, inclusion: { in: [true, false] }
validates :ttl, numericality: { greater_than: 0 }, allow_nil: true
+
+ scope :enabled, -> { where(enabled: true) }
end
diff --git a/app/models/dependency_proxy/manifest.rb b/app/models/dependency_proxy/manifest.rb
index 15e5137b50..b83047efe5 100644
--- a/app/models/dependency_proxy/manifest.rb
+++ b/app/models/dependency_proxy/manifest.rb
@@ -2,6 +2,8 @@
class DependencyProxy::Manifest < ApplicationRecord
include FileStoreMounter
+ include TtlExpirable
+ include EachBatch
belongs_to :group
@@ -9,9 +11,6 @@ class DependencyProxy::Manifest < ApplicationRecord
validates :file, presence: true
validates :file_name, presence: true
validates :digest, presence: true
- validates :status, presence: true
-
- enum status: { default: 0, expired: 1 }
mount_file_store_uploader DependencyProxy::FileUploader
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 4a690ccc67..f91700f764 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -10,7 +10,8 @@ class Deployment < ApplicationRecord
include FastDestroyAll
include IgnorableColumns
- ignore_column :deployable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
+ StatusUpdateError = Class.new(StandardError)
+ StatusSyncError = Class.new(StandardError)
belongs_to :project, required: true
belongs_to :environment, required: true
@@ -48,7 +49,6 @@ class Deployment < ApplicationRecord
scope :stoppable, -> { where.not(on_stop: nil).where.not(deployable_id: nil).success }
scope :active, -> { where(status: %i[created running]) }
scope :older_than, -> (deployment) { where('deployments.id < ?', deployment.id) }
- scope :with_deployable, -> { joins('INNER JOIN ci_builds ON ci_builds.id = deployments.deployable_id').preload(:deployable) }
scope :with_api_entity_associations, -> { preload({ deployable: { runner: [], tags: [], user: [], job_artifacts_archive: [] } }) }
scope :finished_after, ->(date) { where('finished_at >= ?', date) }
@@ -150,6 +150,16 @@ class Deployment < ApplicationRecord
success.find_by!(iid: iid)
end
+ # It should be used with caution especially on chaining.
+ # Fetching any unbounded or large intermediate dataset could lead to loading too many IDs into memory.
+ # See: https://docs.gitlab.com/ee/development/database/multiple_databases.html#use-disable_joins-for-has_one-or-has_many-through-relations
+ # For safety we default limit to fetch not more than 1000 records.
+ def self.builds(limit = 1000)
+ deployable_ids = where.not(deployable_id: nil).limit(limit).pluck(:deployable_id)
+
+ Ci::Build.where(id: deployable_ids)
+ end
+
class << self
##
# FastDestroyAll concerns
@@ -305,20 +315,23 @@ class Deployment < ApplicationRecord
# Changes the status of a deployment and triggers the corresponding state
# machine events.
def update_status(status)
- case status
- when 'running'
- run
- when 'success'
- succeed
- when 'failed'
- drop
- when 'canceled'
- cancel
- when 'skipped'
- skip
- else
- raise ArgumentError, "The status #{status.inspect} is invalid"
- end
+ update_status!(status)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(
+ StatusUpdateError.new(e.message), deployment_id: self.id)
+
+ false
+ end
+
+ def sync_status_with(build)
+ return false unless ::Deployment.statuses.include?(build.status)
+
+ update_status!(build.status)
+ rescue StandardError => e
+ Gitlab::ErrorTracking.track_exception(
+ StatusSyncError.new(e.message), deployment_id: self.id, build_id: build.id)
+
+ false
end
def valid_sha
@@ -346,6 +359,23 @@ class Deployment < ApplicationRecord
private
+ def update_status!(status)
+ case status
+ when 'running'
+ run!
+ when 'success'
+ succeed!
+ when 'failed'
+ drop!
+ when 'canceled'
+ cancel!
+ when 'skipped'
+ skip!
+ else
+ raise ArgumentError, "The status #{status.inspect} is invalid"
+ end
+ end
+
def legacy_finished_at
self.created_at if success? && !read_attribute(:finished_at)
end
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 48522a2306..31ab426728 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -28,8 +28,8 @@ class Environment < ApplicationRecord
has_one :last_deployment, -> { success.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :last_visible_deployment, -> { visible.distinct_on_environment }, inverse_of: :environment, class_name: 'Deployment'
- has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
- has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: -> { ::Feature.enabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml) }
+ has_one :last_visible_deployable, through: :last_visible_deployment, source: 'deployable', source_type: 'CommitStatus', disable_joins: true
+ has_one :last_visible_pipeline, through: :last_visible_deployable, source: 'pipeline', disable_joins: true
has_one :upcoming_deployment, -> { running.distinct_on_environment }, class_name: 'Deployment', inverse_of: :environment
has_one :latest_opened_most_severe_alert, -> { order_severity_with_open_prometheus_alert }, class_name: 'AlertManagement::Alert', inverse_of: :environment
@@ -198,14 +198,14 @@ class Environment < ApplicationRecord
# Overriding association
def last_visible_deployable
- return super if association_cached?(:last_visible_deployable) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
+ return super if association_cached?(:last_visible_deployable)
last_visible_deployment&.deployable
end
# Overriding association
def last_visible_pipeline
- return super if association_cached?(:last_visible_pipeline) || ::Feature.disabled?(:environment_last_visible_pipeline_disable_joins, default_enabled: :yaml)
+ return super if association_cached?(:last_visible_pipeline)
last_visible_deployable&.pipeline
end
@@ -260,10 +260,9 @@ class Environment < ApplicationRecord
end
def cancel_deployment_jobs!
- jobs = active_deployments.with_deployable
- jobs.each do |deployment|
- Gitlab::OptimisticLocking.retry_lock(deployment.deployable, name: 'environment_cancel_deployment_jobs') do |deployable|
- deployable.cancel! if deployable&.cancelable?
+ active_deployments.builds.each do |build|
+ Gitlab::OptimisticLocking.retry_lock(build, name: 'environment_cancel_deployment_jobs') do |build|
+ build.cancel! if build&.cancelable?
end
rescue StandardError => e
Gitlab::ErrorTracking.track_exception(e, environment_id: id, deployment_id: deployment.id)
diff --git a/app/models/environment_status.rb b/app/models/environment_status.rb
index 3be7af2e4b..07c0983f23 100644
--- a/app/models/environment_status.rb
+++ b/app/models/environment_status.rb
@@ -100,13 +100,11 @@ class EnvironmentStatus
def self.build_environments_status(mr, user, pipeline)
return [] unless pipeline
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/340781') do
- pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
- next unless Ability.allowed?(user, :read_environment, environment)
+ pipeline.environments_in_self_and_descendants.includes(:project).available.map do |environment|
+ next unless Ability.allowed?(user, :read_environment, environment)
- EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
- end.compact
- end
+ EnvironmentStatus.new(pipeline.project, environment, mr, pipeline.sha)
+ end.compact
end
private_class_method :build_environments_status
end
diff --git a/app/models/error_tracking/error.rb b/app/models/error_tracking/error.rb
index 39ecc48780..2d6a4694de 100644
--- a/app/models/error_tracking/error.rb
+++ b/app/models/error_tracking/error.rb
@@ -7,6 +7,14 @@ class ErrorTracking::Error < ApplicationRecord
has_many :events, class_name: 'ErrorTracking::ErrorEvent'
+ has_one :first_event,
+ -> { order(id: :asc) },
+ class_name: 'ErrorTracking::ErrorEvent'
+
+ has_one :last_event,
+ -> { order(id: :desc) },
+ class_name: 'ErrorTracking::ErrorEvent'
+
scope :for_status, -> (status) { where(status: status) }
validates :project, presence: true
@@ -90,7 +98,10 @@ class ErrorTracking::Error < ApplicationRecord
status: status,
tags: { level: nil, logger: nil },
external_url: external_url,
- external_base_url: external_base_url
+ external_base_url: external_base_url,
+ integrated: true,
+ first_release_version: first_event&.release,
+ last_release_version: last_event&.release
)
end
@@ -106,6 +117,6 @@ class ErrorTracking::Error < ApplicationRecord
# For compatibility with sentry integration
def external_base_url
- Gitlab::Routing.url_helpers.root_url
+ Gitlab::Routing.url_helpers.project_url(project)
end
end
diff --git a/app/models/error_tracking/error_event.rb b/app/models/error_tracking/error_event.rb
index 4de13de7e2..686518a39f 100644
--- a/app/models/error_tracking/error_event.rb
+++ b/app/models/error_tracking/error_event.rb
@@ -22,6 +22,10 @@ class ErrorTracking::ErrorEvent < ApplicationRecord
)
end
+ def release
+ payload.dig('release')
+ end
+
private
def build_stacktrace
diff --git a/app/models/error_tracking/project_error_tracking_setting.rb b/app/models/error_tracking/project_error_tracking_setting.rb
index dd5ce9f738..25f812645b 100644
--- a/app/models/error_tracking/project_error_tracking_setting.rb
+++ b/app/models/error_tracking/project_error_tracking_setting.rb
@@ -46,6 +46,11 @@ module ErrorTracking
after_save :clear_reactive_cache!
+ # When a user enables the integrated error tracking
+ # we want to immediately provide them with a first
+ # working client key so they have a DSN for Sentry SDK.
+ after_save :create_client_key!
+
def sentry_enabled
enabled && !integrated_client?
end
@@ -54,6 +59,12 @@ module ErrorTracking
integrated
end
+ def gitlab_dsn
+ strong_memoize(:gitlab_dsn) do
+ client_key&.sentry_dsn
+ end
+ end
+
def api_url=(value)
super
clear_memoization(:api_url_slugs)
@@ -236,5 +247,19 @@ module ErrorTracking
errors.add(:project, 'is a required field')
end
end
+
+ def client_key
+ # Project can have multiple client keys.
+ # However for UI simplicity we render the first active one for user.
+ # In future we should make it possible to manage client keys from UI.
+ # Issue https://gitlab.com/gitlab-org/gitlab/-/issues/329596
+ project.error_tracking_client_keys.active.first
+ end
+
+ def create_client_key!
+ if enabled? && integrated_client? && !client_key
+ project.error_tracking_client_keys.create!
+ end
+ end
end
end
diff --git a/app/models/group.rb b/app/models/group.rb
index a667a90870..c5e119451e 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -192,9 +192,15 @@ class Group < Namespace
# Returns the ids of the passed group models where the `emails_disabled`
# column is set to true anywhere in the ancestor hierarchy.
def ids_with_disabled_email(groups)
- innner_query = Gitlab::ObjectHierarchy
- .new(Group.where('id = namespaces_with_emails_disabled.id'))
- .base_and_ancestors
+ inner_groups = Group.where('id = namespaces_with_emails_disabled.id')
+
+ inner_ancestors = if Feature.enabled?(:linear_group_ancestor_scopes, default_enabled: :yaml)
+ inner_groups.self_and_ancestors
+ else
+ Gitlab::ObjectHierarchy.new(inner_groups).base_and_ancestors
+ end
+
+ inner_query = inner_ancestors
.where(emails_disabled: true)
.select('1')
.limit(1)
@@ -202,7 +208,7 @@ class Group < Namespace
group_ids = Namespace
.from('(SELECT * FROM namespaces) as namespaces_with_emails_disabled')
.where(namespaces_with_emails_disabled: { id: groups })
- .where('EXISTS (?)', innner_query)
+ .where('EXISTS (?)', inner_query)
.pluck(:id)
Set.new(group_ids)
@@ -701,9 +707,9 @@ class Group < Namespace
raise ArgumentError unless SHARED_RUNNERS_SETTINGS.include?(state)
case state
- when 'disabled_and_unoverridable' then disable_shared_runners! # also disallows override
- when 'disabled_with_override' then disable_shared_runners_and_allow_override!
- when 'enabled' then enable_shared_runners! # set both to true
+ when SR_DISABLED_AND_UNOVERRIDABLE then disable_shared_runners! # also disallows override
+ when SR_DISABLED_WITH_OVERRIDE then disable_shared_runners_and_allow_override!
+ when SR_ENABLED then enable_shared_runners! # set both to true
end
end
diff --git a/app/models/instance_configuration.rb b/app/models/instance_configuration.rb
index 9565dae08b..0bf9e805aa 100644
--- a/app/models/instance_configuration.rb
+++ b/app/models/instance_configuration.rb
@@ -22,7 +22,12 @@ class InstanceConfiguration
private
def ssh_algorithms_hashes
- SSH_ALGORITHMS.map { |algo| ssh_algorithm_hashes(algo) }.compact
+ SSH_ALGORITHMS.select { |algo| ssh_algorithm_enabled?(algo) }.map { |algo| ssh_algorithm_hashes(algo) }.compact
+ end
+
+ def ssh_algorithm_enabled?(algorithm)
+ algorithm_key_restriction = application_settings["#{algorithm.downcase}_key_restriction"]
+ algorithm_key_restriction.nil? || algorithm_key_restriction != ApplicationSetting::FORBIDDEN_KEY_VALUE
end
def host
@@ -98,6 +103,11 @@ class InstanceConfiguration
requests_per_period: application_settings[:throttle_authenticated_packages_api_requests_per_period],
period_in_seconds: application_settings[:throttle_authenticated_packages_api_period_in_seconds]
},
+ authenticated_git_lfs_api: {
+ enabled: application_settings[:throttle_authenticated_git_lfs_enabled],
+ requests_per_period: application_settings[:throttle_authenticated_git_lfs_requests_per_period],
+ period_in_seconds: application_settings[:throttle_authenticated_git_lfs_period_in_seconds]
+ },
issue_creation: application_setting_limit_per_minute(:issues_create_limit),
note_creation: application_setting_limit_per_minute(:notes_create_limit),
project_export: application_setting_limit_per_minute(:project_export_limit),
diff --git a/app/models/integrations/open_project.rb b/app/models/integrations/open_project.rb
deleted file mode 100644
index e4cfb24151..0000000000
--- a/app/models/integrations/open_project.rb
+++ /dev/null
@@ -1,20 +0,0 @@
-# frozen_string_literal: true
-
-module Integrations
- class OpenProject < BaseIssueTracker
- validates :url, public_url: true, presence: true, if: :activated?
- validates :api_url, public_url: true, allow_blank: true, if: :activated?
- validates :token, presence: true, if: :activated?
- validates :project_identifier_code, presence: true, if: :activated?
-
- data_field :url, :api_url, :token, :closed_status_id, :project_identifier_code
-
- def data_fields
- open_project_tracker_data || self.build_open_project_tracker_data
- end
-
- def self.to_param
- 'open_project'
- end
- end
-end
diff --git a/app/models/integrations/open_project_tracker_data.rb b/app/models/integrations/open_project_tracker_data.rb
deleted file mode 100644
index b3f2618b94..0000000000
--- a/app/models/integrations/open_project_tracker_data.rb
+++ /dev/null
@@ -1,18 +0,0 @@
-# frozen_string_literal: true
-
-module Integrations
- class OpenProjectTrackerData < ApplicationRecord
- include BaseDataFields
-
- # When the Open Project is fresh installed, the default closed status id is "13" based on current version: v8.
- DEFAULT_CLOSED_STATUS_ID = "13"
-
- attr_encrypted :url, encryption_options
- attr_encrypted :api_url, encryption_options
- attr_encrypted :token, encryption_options
-
- def closed_status_id
- super || DEFAULT_CLOSED_STATUS_ID
- end
- end
-end
diff --git a/app/models/integrations/unify_circuit.rb b/app/models/integrations/unify_circuit.rb
index ad6a9164d0..e3e180ae95 100644
--- a/app/models/integrations/unify_circuit.rb
+++ b/app/models/integrations/unify_circuit.rb
@@ -15,13 +15,8 @@ module Integrations
end
def help
- 'This service sends notifications about projects events to a Unify Circuit conversation.
- To set up this service:
-
- - Set up an incoming webhook for your conversation. All notifications will come to this conversation.
- - Paste the Webhook URL into the field below.
- - Select events below to enable notifications.
-
'
+ docs_link = ActionController::Base.helpers.link_to _('How do I set up this service?'), Rails.application.routes.url_helpers.help_page_url('user/project/integrations/unify_circuit'), target: '_blank', rel: 'noopener noreferrer'
+ s_('Integrations|Send notifications about project events to a Unify Circuit conversation. %{docs_link}').html_safe % { docs_link: docs_link.html_safe }
end
def event_field(event)
@@ -37,7 +32,7 @@ module Integrations
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. https://circuit.com/rest/v2/webhooks/incoming/…", required: true },
+ { type: 'text', name: 'webhook', placeholder: "https://yourcircuit.com/rest/v2/webhooks/incoming/…", required: true },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'select', name: 'branches_to_be_notified', choices: branch_choices }
]
diff --git a/app/models/issue.rb b/app/models/issue.rb
index e0b0c352c2..9c568414ec 100644
--- a/app/models/issue.rb
+++ b/app/models/issue.rb
@@ -81,6 +81,7 @@ class Issue < ApplicationRecord
has_and_belongs_to_many :self_managed_prometheus_alert_events, join_table: :issues_self_managed_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_and_belongs_to_many :prometheus_alert_events, join_table: :issues_prometheus_alert_events # rubocop: disable Rails/HasAndBelongsToMany
has_many :prometheus_alerts, through: :prometheus_alert_events
+ has_and_belongs_to_many :customer_relations_contacts, join_table: :issue_customer_relations_contacts, class_name: 'CustomerRelations::Contact' # rubocop: disable Rails/HasAndBelongsToMany
accepts_nested_attributes_for :issuable_severity, update_only: true
accepts_nested_attributes_for :sentry_issue
@@ -107,8 +108,6 @@ class Issue < ApplicationRecord
scope :order_due_date_asc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'ASC')) }
scope :order_due_date_desc, -> { reorder(::Gitlab::Database.nulls_last_order('due_date', 'DESC')) }
scope :order_closest_future_date, -> { reorder(Arel.sql('CASE WHEN issues.due_date >= CURRENT_DATE THEN 0 ELSE 1 END ASC, ABS(CURRENT_DATE - issues.due_date) ASC')) }
- scope :order_relative_position_asc, -> { reorder(::Gitlab::Database.nulls_last_order('relative_position', 'ASC')) }
- scope :order_relative_position_desc, -> { reorder(::Gitlab::Database.nulls_first_order('relative_position', 'DESC')) }
scope :order_closed_date_desc, -> { reorder(closed_at: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc) }
scope :order_severity_asc, -> { includes(:issuable_severity).order('issuable_severities.severity ASC NULLS FIRST') }
@@ -127,6 +126,7 @@ class Issue < ApplicationRecord
project: [:route, { namespace: :route }])
}
scope :with_issue_type, ->(types) { where(issue_type: types) }
+ scope :without_issue_type, ->(types) { where.not(issue_type: types) }
scope :public_only, -> {
without_hidden.where(confidential: false)
@@ -166,6 +166,8 @@ class Issue < ApplicationRecord
scope :by_project_id_and_iid, ->(composites) do
where_composite(%i[project_id iid], composites)
end
+ scope :with_null_relative_position, -> { where(relative_position: nil) }
+ scope :with_non_null_relative_position, -> { where.not(relative_position: nil) }
after_commit :expire_etag_cache, unless: :importing?
after_save :ensure_metrics, unless: :importing?
@@ -266,8 +268,8 @@ class Issue < ApplicationRecord
'due_date' => -> { order_due_date_asc.with_order_id_desc },
'due_date_asc' => -> { order_due_date_asc.with_order_id_desc },
'due_date_desc' => -> { order_due_date_desc.with_order_id_desc },
- 'relative_position' => -> { order_relative_position_asc.with_order_id_desc },
- 'relative_position_asc' => -> { order_relative_position_asc.with_order_id_desc }
+ 'relative_position' => -> { order_by_relative_position },
+ 'relative_position_asc' => -> { order_by_relative_position }
}
)
end
@@ -277,7 +279,7 @@ class Issue < ApplicationRecord
when 'closest_future_date', 'closest_future_date_asc' then order_closest_future_date
when 'due_date', 'due_date_asc' then order_due_date_asc.with_order_id_desc
when 'due_date_desc' then order_due_date_desc.with_order_id_desc
- when 'relative_position', 'relative_position_asc' then order_relative_position_asc.with_order_id_desc
+ when 'relative_position', 'relative_position_asc' then order_by_relative_position
when 'severity_asc' then order_severity_asc.with_order_id_desc
when 'severity_desc' then order_severity_desc.with_order_id_desc
else
@@ -285,13 +287,8 @@ class Issue < ApplicationRecord
end
end
- # `with_cte` argument allows sorting when using CTE queries and prevents
- # errors in postgres when using CTE search optimisation
- def self.order_by_position_and_priority(with_cte: false)
- order = Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_highest_priority, column_order_id_desc])
-
- order_labels_priority(with_cte: with_cte)
- .reorder(order)
+ def self.order_by_relative_position
+ reorder(Gitlab::Pagination::Keyset::Order.build([column_order_relative_position, column_order_id_asc]))
end
def self.column_order_relative_position
@@ -306,25 +303,6 @@ class Issue < ApplicationRecord
)
end
- def self.column_order_highest_priority
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'highest_priority',
- column_expression: Arel.sql('highest_priorities.label_priority'),
- order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'ASC'),
- reversed_order_expression: Gitlab::Database.nulls_last_order('highest_priorities.label_priority', 'DESC'),
- order_direction: :asc,
- nullable: :nulls_last,
- distinct: false
- )
- end
-
- def self.column_order_id_desc
- Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
- attribute_name: 'id',
- order_expression: arel_table[:id].desc
- )
- end
-
def self.column_order_id_asc
Gitlab::Pagination::Keyset::ColumnOrderDefinition.new(
attribute_name: 'id',
@@ -541,6 +519,10 @@ class Issue < ApplicationRecord
issue_type_supports?(:time_tracking)
end
+ def supports_move_and_clone?
+ issue_type_supports?(:move_and_clone)
+ end
+
def email_participants_emails
issue_email_participants.pluck(:email)
end
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index 25e90036a5..ede2057885 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -13,7 +13,7 @@ class LegacyDiffNote < Note
validates :line_code, presence: true, line_code: true
- before_create :set_diff
+ before_create :set_diff, unless: :skip_setting_st_diff?
def discussion_class(*)
LegacyDiffDiscussion
@@ -90,6 +90,10 @@ class LegacyDiffNote < Note
self.st_diff = diff.to_hash if diff
end
+ def skip_setting_st_diff?
+ st_diff.present? && importing?
+ end
+
def diff_for_line_code
attributes = {
noteable_type: noteable_type,
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 53e7d52c55..9765ac6f2e 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -49,7 +49,7 @@ class LfsObject < ApplicationRecord
end
def self.calculate_oid(path)
- self.hexdigest(path)
+ self.sha256_hexdigest(path)
end
end
diff --git a/app/models/loose_foreign_keys/deleted_record.rb b/app/models/loose_foreign_keys/deleted_record.rb
index a39d88b2e4..ca5a2800a0 100644
--- a/app/models/loose_foreign_keys/deleted_record.rb
+++ b/app/models/loose_foreign_keys/deleted_record.rb
@@ -2,48 +2,4 @@
class LooseForeignKeys::DeletedRecord < ApplicationRecord
extend SuppressCompositePrimaryKeyWarning
- include PartitionedTable
-
- partitioned_by :created_at, strategy: :monthly, retain_for: 3.months, retain_non_empty_partitions: true
-
- scope :ordered_by_primary_keys, -> { order(:created_at, :deleted_table_name, :deleted_table_primary_key_value) }
-
- def self.load_batch(batch_size)
- ordered_by_primary_keys
- .limit(batch_size)
- .to_a
- end
-
- # Because the table has composite primary keys, the delete_all or delete methods are not going to work.
- # This method implements deletion that benefits from the primary key index, example:
- #
- # > DELETE
- # > FROM "loose_foreign_keys_deleted_records"
- # > WHERE (created_at,
- # > deleted_table_name,
- # > deleted_table_primary_key_value) IN
- # > (SELECT created_at::TIMESTAMP WITH TIME ZONE,
- # > deleted_table_name,
- # > deleted_table_primary_key_value
- # > FROM (VALUES (LIST_OF_VALUES)) AS primary_key_values (created_at, deleted_table_name, deleted_table_primary_key_value))
- def self.delete_records(records)
- values = records.pluck(:created_at, :deleted_table_name, :deleted_table_primary_key_value)
-
- primary_keys = connection.primary_keys(table_name).join(', ')
-
- primary_keys_with_type_cast = [
- Arel.sql('created_at::timestamp with time zone'),
- Arel.sql('deleted_table_name'),
- Arel.sql('deleted_table_primary_key_value')
- ]
-
- value_list = Arel::Nodes::ValuesList.new(values)
-
- # (SELECT primary keys FROM VALUES)
- inner_query = Arel::SelectManager.new
- inner_query.from("#{Arel::Nodes::Grouping.new([value_list]).as('primary_key_values').to_sql} (#{primary_keys})")
- inner_query.projections = primary_keys_with_type_cast
-
- where(Arel::Nodes::Grouping.new([Arel.sql(primary_keys)]).in(inner_query)).delete_all
- end
end
diff --git a/app/models/member.rb b/app/models/member.rb
index beb4c05f2a..21fd4aebd7 100644
--- a/app/models/member.rb
+++ b/app/models/member.rb
@@ -50,6 +50,11 @@ class Member < ApplicationRecord
},
if: :project_bot?
+ scope :with_invited_user_state, -> do
+ joins('LEFT JOIN users as invited_user ON invited_user.email = members.invite_email')
+ .select('members.*', 'invited_user.state as invited_user_state')
+ end
+
scope :in_hierarchy, ->(source) do
groups = source.root_ancestor.self_and_descendants
group_members = Member.default_scoped.where(source: groups)
@@ -178,7 +183,13 @@ class Member < ApplicationRecord
after_destroy :post_destroy_hook, unless: :pending?, if: :hook_prerequisites_met?
after_save :log_invitation_token_cleanup
- after_commit :refresh_member_authorized_projects, unless: :importing?
+ after_commit on: [:create, :update], unless: :importing? do
+ refresh_member_authorized_projects(blocking: true)
+ end
+
+ after_commit on: [:destroy], unless: :importing? do
+ refresh_member_authorized_projects(blocking: false)
+ end
default_value_for :notification_level, NotificationSetting.levels[:global]
@@ -395,8 +406,8 @@ class Member < ApplicationRecord
# transaction has been committed, resulting in the job either throwing an
# error or not doing any meaningful work.
# rubocop: disable CodeReuse/ServiceClass
- def refresh_member_authorized_projects
- UserProjectAccessChangedService.new(user_id).execute
+ def refresh_member_authorized_projects(blocking:)
+ UserProjectAccessChangedService.new(user_id).execute(blocking: blocking)
end
# rubocop: enable CodeReuse/ServiceClass
@@ -442,6 +453,14 @@ class Member < ApplicationRecord
errors.add(:user, error) if error
end
+ def signup_email_invalid_message
+ if source_type == 'Project'
+ _("is not allowed for this project.")
+ else
+ _("is not allowed for this group.")
+ end
+ end
+
def update_highest_role?
return unless user_id.present?
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index a13133c90e..9062a40521 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -43,15 +43,17 @@ class GroupMember < Member
# Because source_type is `Namespace`...
def real_source_type
- 'Group'
+ Group.sti_name
end
def notifiable_options
{ group: group }
end
+ private
+
override :refresh_member_authorized_projects
- def refresh_member_authorized_projects
+ def refresh_member_authorized_projects(blocking:)
# Here, `destroyed_by_association` will be present if the
# GroupMember is being destroyed due to the `dependent: :destroy`
# callback on Group. In this case, there is no need to refresh the
@@ -63,8 +65,6 @@ class GroupMember < Member
super
end
- private
-
def access_level_inclusion
return if access_level.in?(Gitlab::Access.all_values)
diff --git a/app/models/members/project_member.rb b/app/models/members/project_member.rb
index 72cb831cc8..eec46b3493 100644
--- a/app/models/members/project_member.rb
+++ b/app/models/members/project_member.rb
@@ -90,24 +90,28 @@ class ProjectMember < Member
{ project: project }
end
+ private
+
override :refresh_member_authorized_projects
- def refresh_member_authorized_projects
+ def refresh_member_authorized_projects(blocking:)
return super unless Feature.enabled?(:specialized_service_for_project_member_auth_refresh)
return unless user
# rubocop:disable CodeReuse/ServiceClass
- AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+ if blocking
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+ else
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker.perform_async(project.id, user.id)
+ end
# Until we compare the inconsistency rates of the new, specialized service and
# the old approach, we still run AuthorizedProjectsWorker
# but with some delay and lower urgency as a safety net.
UserProjectAccessChangedService.new(user_id)
- .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
+ .execute(blocking: false, priority: UserProjectAccessChangedService::LOW_PRIORITY)
# rubocop:enable CodeReuse/ServiceClass
end
- private
-
def send_invite
run_after_commit_or_now { notification_service.invite_project_member(self, @raw_invite_token) }
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index db49ec6f41..15862fb2bf 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -1111,15 +1111,23 @@ class MergeRequest < ApplicationRecord
can_be_merged? && !should_be_rebased?
end
+ # rubocop: disable CodeReuse/ServiceClass
def mergeable_state?(skip_ci_check: false, skip_discussions_check: false)
return false unless open?
return false if work_in_progress?
return false if broken?
- return false unless skip_ci_check || mergeable_ci_state?
return false unless skip_discussions_check || mergeable_discussions_state?
- true
+ if Feature.enabled?(:improved_mergeability_checks, self.project, default_enabled: :yaml)
+ additional_checks = MergeRequests::Mergeability::RunChecksService.new(merge_request: self, params: { skip_ci_check: skip_ci_check })
+ additional_checks.execute.all?(&:success?)
+ else
+ return false unless skip_ci_check || mergeable_ci_state?
+
+ true
+ end
end
+ # rubocop: enable CodeReuse/ServiceClass
def ff_merge_possible?
project.repository.ancestor?(target_branch_sha, diff_head_sha)
@@ -1658,6 +1666,10 @@ class MergeRequest < ApplicationRecord
service_class.new(project, current_user, id: id, report_type: report_type).execute(comparison_base_pipeline(identifier), actual_head_pipeline)
end
+ def recent_diff_head_shas(limit = 100)
+ merge_request_diffs.recent(limit).pluck(:head_commit_sha)
+ end
+
def all_commits
MergeRequestDiffCommit
.where(merge_request_diff: merge_request_diffs.recent)
@@ -1857,7 +1869,7 @@ class MergeRequest < ApplicationRecord
override :ensure_metrics
def ensure_metrics
- if Feature.enabled?(:use_upsert_query_for_mr_metrics)
+ if Feature.enabled?(:use_upsert_query_for_mr_metrics, default_enabled: :yaml)
MergeRequest::Metrics.record!(self)
else
# Backward compatibility: some merge request metrics records will not have target_project_id filled in.
@@ -1918,20 +1930,6 @@ class MergeRequest < ApplicationRecord
end
end
- def lazy_upvotes_count
- BatchLoader.for(id).batch(default_value: 0) do |ids, loader|
- counts = AwardEmoji
- .where(awardable_id: ids)
- .upvotes
- .group(:awardable_id)
- .count
-
- counts.each do |id, count|
- loader.call(id, count)
- end
- end
- end
-
private
def set_draft_status
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index d2b3ca753b..bd94c0ad30 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -66,7 +66,7 @@ class MergeRequestDiff < ApplicationRecord
joins(:merge_request).where(merge_requests: { target_project_id: project_id })
end
- scope :recent, -> { order(id: :desc).limit(100) }
+ scope :recent, -> (limit = 100) { order(id: :desc).limit(limit) }
scope :files_in_database, -> do
where(stored_externally: [false, nil]).where(arel_table[:files_count].gt(0))
diff --git a/app/models/metrics/dashboard/annotation.rb b/app/models/metrics/dashboard/annotation.rb
index 3383dda20c..d3d3f97339 100644
--- a/app/models/metrics/dashboard/annotation.rb
+++ b/app/models/metrics/dashboard/annotation.rb
@@ -32,19 +32,19 @@ module Metrics
def ending_at_after_starting_at
return if ending_at.blank? || starting_at.blank? || starting_at <= ending_at
- errors.add(:ending_at, s_("Metrics::Dashboard::Annotation|can't be before starting_at time"))
+ errors.add(:ending_at, s_("MetricsDashboardAnnotation|can't be before starting_at time"))
end
def single_ownership
return if cluster.nil? ^ environment.nil?
- errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation can't belong to both a cluster and an environment at the same time"))
+ errors.add(:base, s_("MetricsDashboardAnnotation|Annotation can't belong to both a cluster and an environment at the same time"))
end
def orphaned_annotation
return if cluster.present? || environment.present?
- errors.add(:base, s_("Metrics::Dashboard::Annotation|Annotation must belong to a cluster or an environment"))
+ errors.add(:base, s_("MetricsDashboardAnnotation|Annotation must belong to a cluster or an environment"))
end
end
end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index aba61fb7ba..07f9bb9995 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -28,7 +28,10 @@ class Namespace < ApplicationRecord
# Android repo (15) + some extra backup.
NUMBER_OF_ANCESTORS_ALLOWED = 20
- SHARED_RUNNERS_SETTINGS = %w[disabled_and_unoverridable disabled_with_override enabled].freeze
+ SR_DISABLED_AND_UNOVERRIDABLE = 'disabled_and_unoverridable'
+ SR_DISABLED_WITH_OVERRIDE = 'disabled_with_override'
+ SR_ENABLED = 'enabled'
+ SHARED_RUNNERS_SETTINGS = [SR_DISABLED_AND_UNOVERRIDABLE, SR_DISABLED_WITH_OVERRIDE, SR_ENABLED].freeze
URL_MAX_LENGTH = 255
PATH_TRAILING_VIOLATIONS = %w[.git .atom .].freeze
@@ -46,6 +49,8 @@ class Namespace < ApplicationRecord
# This should _not_ be `inverse_of: :namespace`, because that would also set
# `user.namespace` when this user creates a group with themselves as `owner`.
+ # TODO: can this be moved into the UserNamespace class?
+ # evaluate in issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
belongs_to :owner, class_name: "User"
belongs_to :parent, class_name: "Namespace"
@@ -65,21 +70,31 @@ class Namespace < ApplicationRecord
length: { maximum: 255 }
validates :description, length: { maximum: 255 }
+
validates :path,
presence: true,
- length: { maximum: URL_MAX_LENGTH },
- namespace_path: true
+ length: { maximum: URL_MAX_LENGTH }
+
+ validates :path, namespace_path: true, if: ->(n) { !n.project_namespace? }
+ # Project path validator is used for project namespaces for now to assure
+ # compatibility with project paths
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764
+ validates :path, project_path: true, if: ->(n) { n.project_namespace? }
# Introduce minimal path length of 2 characters.
# Allow change of other attributes without forcing users to
# rename their user or group. At the same time prevent changing
# the path without complying with new 2 chars requirement.
# Issue https://gitlab.com/gitlab-org/gitlab/-/issues/225214
- validates :path, length: { minimum: 2 }, if: :path_changed?
+ #
+ # For ProjectNamespace we don't check minimal path length to keep
+ # compatibility with existing project restrictions.
+ # Issue: https://gitlab.com/gitlab-org/gitlab/-/issues/341764
+ validates :path, length: { minimum: 2 }, if: :enforce_minimum_path_length?
validates :max_artifacts_size, numericality: { only_integer: true, greater_than: 0, allow_nil: true }
- validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type) }
+ validate :validate_parent_type, if: -> { Feature.enabled?(:validate_namespace_parent_type, default_enabled: :yaml) }
validate :nesting_level_allowed
validate :changing_shared_runners_enabled_is_allowed
validate :changing_allow_descendants_override_disabled_shared_runners_is_allowed
@@ -95,7 +110,7 @@ class Namespace < ApplicationRecord
# Legacy Storage specific hooks
- after_update :move_dir, if: :saved_change_to_path_or_parent?
+ after_update :move_dir, if: :saved_change_to_path_or_parent?, unless: -> { is_a?(Namespaces::ProjectNamespace) }
before_destroy(prepend: true) { prepare_for_destroy }
after_destroy :rm_dir
after_commit :expire_child_caches, on: :update, if: -> {
@@ -103,7 +118,12 @@ class Namespace < ApplicationRecord
saved_change_to_name? || saved_change_to_path? || saved_change_to_parent_id?
}
- scope :for_user, -> { where(type: nil) }
+ # TODO: change to `type: Namespaces::UserNamespace.sti_name` when
+ # working on issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
+ scope :user_namespaces, -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) }
+ # TODO: this can be simplified with `type != 'Project'` when working on issue
+ # https://gitlab.com/gitlab-org/gitlab/-/issues/341070
+ scope :without_project_namespaces, -> { where("type IS DISTINCT FROM ?", Namespaces::ProjectNamespace.sti_name) }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :include_route, -> { includes(:route) }
scope :by_parent, -> (parent) { where(parent_id: parent) }
@@ -140,14 +160,12 @@ class Namespace < ApplicationRecord
class << self
def sti_class_for(type_name)
case type_name
- when 'Group'
+ when Group.sti_name
Group
- when 'Project'
+ when Namespaces::ProjectNamespace.sti_name
Namespaces::ProjectNamespace
- when 'User'
- # TODO: We create a normal Namespace until
- # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68894 is ready
- Namespace
+ when Namespaces::UserNamespace.sti_name
+ Namespaces::UserNamespace
else
Namespace
end
@@ -254,27 +272,27 @@ class Namespace < ApplicationRecord
end
def kind
- return 'group' if group?
- return 'project' if project?
+ return 'group' if group_namespace?
+ return 'project' if project_namespace?
'user' # defaults to user
end
- def group?
+ def group_namespace?
type == Group.sti_name
end
- def project?
+ def project_namespace?
type == Namespaces::ProjectNamespace.sti_name
end
- def user?
+ def user_namespace?
# That last bit ensures we're considered a user namespace as a default
- type.nil? || type == Namespaces::UserNamespace.sti_name || !(group? || project?)
+ type.nil? || type == Namespaces::UserNamespace.sti_name || !(group_namespace? || project_namespace?)
end
def owner_required?
- user?
+ user_namespace?
end
def find_fork_of(project)
@@ -321,7 +339,7 @@ class Namespace < ApplicationRecord
# that belongs to this namespace
def all_projects
if Feature.enabled?(:recursive_approach_for_all_projects, default_enabled: :yaml)
- namespace = user? ? self : self_and_descendant_ids
+ namespace = user_namespace? ? self : self_and_descendant_ids
Project.where(namespace: namespace)
else
Project.inside_path(full_path)
@@ -423,7 +441,7 @@ class Namespace < ApplicationRecord
def changing_shared_runners_enabled_is_allowed
return unless new_record? || changes.has_key?(:shared_runners_enabled)
- if shared_runners_enabled && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ if shared_runners_enabled && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE
errors.add(:shared_runners_enabled, _('cannot be enabled because parent group has shared Runners disabled'))
end
end
@@ -435,30 +453,30 @@ class Namespace < ApplicationRecord
errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be changed if shared runners are enabled'))
end
- if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == 'disabled_and_unoverridable'
+ if allow_descendants_override_disabled_shared_runners && has_parent? && parent.shared_runners_setting == SR_DISABLED_AND_UNOVERRIDABLE
errors.add(:allow_descendants_override_disabled_shared_runners, _('cannot be enabled because parent group does not allow it'))
end
end
def shared_runners_setting
if shared_runners_enabled
- 'enabled'
+ SR_ENABLED
else
if allow_descendants_override_disabled_shared_runners
- 'disabled_with_override'
+ SR_DISABLED_WITH_OVERRIDE
else
- 'disabled_and_unoverridable'
+ SR_DISABLED_AND_UNOVERRIDABLE
end
end
end
def shared_runners_setting_higher_than?(other_setting)
- if other_setting == 'enabled'
+ if other_setting == SR_ENABLED
false
- elsif other_setting == 'disabled_with_override'
- shared_runners_setting == 'enabled'
- elsif other_setting == 'disabled_and_unoverridable'
- shared_runners_setting == 'enabled' || shared_runners_setting == 'disabled_with_override'
+ elsif other_setting == SR_DISABLED_WITH_OVERRIDE
+ shared_runners_setting == SR_ENABLED
+ elsif other_setting == SR_DISABLED_AND_UNOVERRIDABLE
+ shared_runners_setting == SR_ENABLED || shared_runners_setting == SR_DISABLED_WITH_OVERRIDE
else
raise ArgumentError
end
@@ -543,21 +561,21 @@ class Namespace < ApplicationRecord
def validate_parent_type
unless has_parent?
- if project?
+ if project_namespace?
errors.add(:parent_id, _('must be set for a project namespace'))
end
return
end
- if parent.project?
+ if parent.project_namespace?
errors.add(:parent_id, _('project namespace cannot be the parent of another namespace'))
end
- if user?
+ if user_namespace?
errors.add(:parent_id, _('cannot not be used for user namespace'))
- elsif group?
- errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user?
+ elsif group_namespace?
+ errors.add(:parent_id, _('user namespace cannot be the parent of another namespace')) if parent.user_namespace?
end
end
@@ -582,6 +600,10 @@ class Namespace < ApplicationRecord
project.track_project_repository
end
end
+
+ def enforce_minimum_path_length?
+ path_changed? && !project_namespace?
+ end
end
Namespace.prepend_mod_with('Namespace')
diff --git a/app/models/namespace/root_storage_statistics.rb b/app/models/namespace/root_storage_statistics.rb
index 73061b7863..99e3253759 100644
--- a/app/models/namespace/root_storage_statistics.rb
+++ b/app/models/namespace/root_storage_statistics.rb
@@ -57,7 +57,7 @@ class Namespace::RootStorageStatistics < ApplicationRecord
end
def attributes_from_personal_snippets
- return {} unless namespace.user?
+ return {} unless namespace.user_namespace?
from_personal_snippets.take.slice(SNIPPETS_SIZE_STAT_NAME)
end
diff --git a/app/models/namespaces/user_namespace.rb b/app/models/namespaces/user_namespace.rb
index 517d68b118..22b7a0a3b2 100644
--- a/app/models/namespaces/user_namespace.rb
+++ b/app/models/namespaces/user_namespace.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
# TODO: currently not created/mapped in the database, will be done in another issue
-# https://gitlab.com/gitlab-org/gitlab/-/issues/337102
+# https://gitlab.com/gitlab-org/gitlab/-/issues/341070
module Namespaces
class UserNamespace < Namespace
def self.sti_name
diff --git a/app/models/note.rb b/app/models/note.rb
index a8f5c305d9..3747351889 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -149,7 +149,7 @@ class Note < ApplicationRecord
scope :like_note_or_capitalized_note, ->(text) { where('(note LIKE ? OR note LIKE ?)', text, text.capitalize) }
before_validation :nullify_blank_type, :nullify_blank_line_code
- after_save :keep_around_commit, if: :for_project_noteable?, unless: :importing?
+ after_save :keep_around_commit, if: :for_project_noteable?, unless: -> { importing? || skip_keep_around_commits }
after_save :expire_etag_cache, unless: :importing?
after_save :touch_noteable, unless: :importing?
after_destroy :expire_etag_cache
@@ -355,8 +355,6 @@ class Note < ApplicationRecord
end
def noteable_author?(noteable)
- return false unless ::Feature.enabled?(:show_author_on_note, project)
-
noteable.author == self.author
end
diff --git a/app/models/operations/feature_flag.rb b/app/models/operations/feature_flag.rb
index 46810749b1..7db396bcad 100644
--- a/app/models/operations/feature_flag.rb
+++ b/app/models/operations/feature_flag.rb
@@ -19,13 +19,10 @@ module Operations
default_value_for :active, true
default_value_for :version, :new_version_flag
- # scopes exists only for the first version
- has_many :scopes, class_name: 'Operations::FeatureFlagScope'
# strategies exists only for the second version
has_many :strategies, class_name: 'Operations::FeatureFlags::Strategy'
has_many :feature_flag_issues
has_many :issues, through: :feature_flag_issues
- has_one :default_scope, -> { where(environment_scope: '*') }, class_name: 'Operations::FeatureFlagScope'
validates :project, presence: true
validates :name,
@@ -37,10 +34,7 @@ module Operations
}
validates :name, uniqueness: { scope: :project_id }
validates :description, allow_blank: true, length: 0..255
- validate :first_default_scope, on: :create, if: :has_scopes?
- validate :version_associations
- accepts_nested_attributes_for :scopes, allow_destroy: true
accepts_nested_attributes_for :strategies, allow_destroy: true
scope :ordered, -> { order(:name) }
@@ -56,7 +50,7 @@ module Operations
class << self
def preload_relations
- preload(:scopes, strategies: :scopes)
+ preload(strategies: :scopes)
end
def for_unleash_client(project, environment)
@@ -104,13 +98,6 @@ module Operations
Ability.issues_readable_by_user(issues, current_user)
end
- def execute_hooks(current_user)
- run_after_commit do
- feature_flag_data = Gitlab::DataBuilder::FeatureFlag.build(self, current_user)
- project.execute_hooks(feature_flag_data, :feature_flag_hooks)
- end
- end
-
def hook_attrs
{
id: id,
@@ -119,27 +106,5 @@ module Operations
active: active
}
end
-
- private
-
- def version_associations
- if new_version_flag? && scopes.any?
- errors.add(:version_associations, 'version 2 feature flags may not have scopes')
- end
- end
-
- def first_default_scope
- unless scopes.first.environment_scope == '*'
- errors.add(:default_scope, 'has to be the first element')
- end
- end
-
- def build_default_scope
- scopes.build(environment_scope: '*', active: self.active)
- end
-
- def has_scopes?
- scopes.any?
- end
end
end
diff --git a/app/models/operations/feature_flag_scope.rb b/app/models/operations/feature_flag_scope.rb
deleted file mode 100644
index 9068ca0f58..0000000000
--- a/app/models/operations/feature_flag_scope.rb
+++ /dev/null
@@ -1,66 +0,0 @@
-# frozen_string_literal: true
-
-# All of the legacy flags have been removed in 14.1, including all of the
-# `operations_feature_flag_scopes` rows. Therefore, this model and the database
-# table are unused and should be removed.
-
-module Operations
- class FeatureFlagScope < ApplicationRecord
- prepend HasEnvironmentScope
- include Gitlab::Utils::StrongMemoize
-
- self.table_name = 'operations_feature_flag_scopes'
-
- belongs_to :feature_flag
-
- validates :environment_scope, uniqueness: {
- scope: :feature_flag,
- message: "(%{value}) has already been taken"
- }
-
- validates :environment_scope,
- if: :default_scope?, on: :update,
- inclusion: { in: %w(*), message: 'cannot be changed from default scope' }
-
- validates :strategies, feature_flag_strategies: true
-
- before_destroy :prevent_destroy_default_scope, if: :default_scope?
-
- scope :ordered, -> { order(:id) }
- scope :enabled, -> { where(active: true) }
- scope :disabled, -> { where(active: false) }
-
- def self.with_name_and_description
- joins(:feature_flag)
- .select(FeatureFlag.arel_table[:name], FeatureFlag.arel_table[:description])
- end
-
- def self.for_unleash_client(project, environment)
- select_columns = [
- 'DISTINCT ON (operations_feature_flag_scopes.feature_flag_id) operations_feature_flag_scopes.id',
- '(operations_feature_flags.active AND operations_feature_flag_scopes.active) AS active',
- 'operations_feature_flag_scopes.strategies',
- 'operations_feature_flag_scopes.environment_scope',
- 'operations_feature_flag_scopes.created_at',
- 'operations_feature_flag_scopes.updated_at'
- ]
-
- select(select_columns)
- .with_name_and_description
- .where(feature_flag_id: project.operations_feature_flags.select(:id))
- .order(:feature_flag_id)
- .on_environment(environment)
- .reverse_order
- end
-
- private
-
- def default_scope?
- environment_scope_was == '*'
- end
-
- def prevent_destroy_default_scope
- raise ActiveRecord::ReadOnlyRecord, "default scope cannot be destroyed"
- end
- end
-end
diff --git a/app/models/packages/composer/cache_file.rb b/app/models/packages/composer/cache_file.rb
index ecd7596b98..5222101d17 100644
--- a/app/models/packages/composer/cache_file.rb
+++ b/app/models/packages/composer/cache_file.rb
@@ -9,15 +9,13 @@ module Packages
mount_file_store_uploader Packages::Composer::CacheUploader
- belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
+ belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id'
belongs_to :namespace
validates :namespace, presence: true
scope :with_namespace, ->(namespace) { where(namespace: namespace) }
scope :with_sha, ->(sha) { where(file_sha256: sha) }
- scope :expired, -> { where("delete_at <= ?", Time.current) }
- scope :without_namespace, -> { where(namespace_id: nil) }
end
end
end
diff --git a/app/models/packages/helm/file_metadatum.rb b/app/models/packages/helm/file_metadatum.rb
index 1771003d1f..dfa4ab6df8 100644
--- a/app/models/packages/helm/file_metadatum.rb
+++ b/app/models/packages/helm/file_metadatum.rb
@@ -12,7 +12,7 @@ module Packages
validates :channel,
presence: true,
- length: { maximum: 63 },
+ length: { maximum: 255 },
format: { with: Gitlab::Regex.helm_channel_regex }
validates :metadata,
diff --git a/app/models/pages_domain.rb b/app/models/pages_domain.rb
index c932d0bf80..0c5a155d48 100644
--- a/app/models/pages_domain.rb
+++ b/app/models/pages_domain.rb
@@ -129,18 +129,15 @@ class PagesDomain < ApplicationRecord
store = OpenSSL::X509::Store.new
store.set_default_paths
- # This forces to load all intermediate certificates stored in `certificate`
- Tempfile.open('certificate_chain') do |f|
- f.write(certificate)
- f.flush
- store.add_file(f.path)
- end
-
- store.verify(x509)
+ store.verify(x509, untrusted_ca_certs_bundle)
rescue OpenSSL::X509::StoreError
false
end
+ def untrusted_ca_certs_bundle
+ ::Gitlab::X509::Certificate.load_ca_certs_bundle(certificate)
+ end
+
def expired?
return false unless x509
diff --git a/app/models/preloaders/merge_requests_preloader.rb b/app/models/preloaders/merge_requests_preloader.rb
deleted file mode 100644
index cefe8408ca..0000000000
--- a/app/models/preloaders/merge_requests_preloader.rb
+++ /dev/null
@@ -1,19 +0,0 @@
-# frozen_string_literal: true
-
-module Preloaders
- class MergeRequestsPreloader
- attr_reader :merge_requests
-
- def initialize(merge_requests)
- @merge_requests = merge_requests
- end
-
- def execute
- preloader = ActiveRecord::Associations::Preloader.new
- preloader.preload(merge_requests, { target_project: [:project_feature] })
- merge_requests.each do |merge_request|
- merge_request.lazy_upvotes_count
- end
- end
- end
-end
diff --git a/app/models/product_analytics_event.rb b/app/models/product_analytics_event.rb
index d2026d3b33..52baa3be6c 100644
--- a/app/models/product_analytics_event.rb
+++ b/app/models/product_analytics_event.rb
@@ -20,8 +20,6 @@ class ProductAnalyticsEvent < ApplicationRecord
where('collector_tstamp BETWEEN ? AND ? ', today - duration + 1, today + 1)
}
- scope :by_category_and_action, ->(category, action) { where(se_category: category, se_action: action) }
-
def self.count_by_graph(graph, days)
group(graph).timerange(days).count
end
diff --git a/app/models/project.rb b/app/models/project.rb
index f68fdadf51..2ceba10e86 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -98,6 +98,7 @@ class Project < ApplicationRecord
before_validation :mark_remote_mirrors_for_removal, if: -> { RemoteMirror.table_exists? }
before_save :ensure_runners_token
+ before_save :ensure_project_namespace_in_sync
after_save :update_project_statistics, if: :saved_change_to_namespace_id?
@@ -128,26 +129,9 @@ class Project < ApplicationRecord
after_initialize :use_hashed_storage
after_create :check_repository_absence!
- # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
- # TODO: remove 'acts_as_ordered_taggable_on' and ':topics_acts_as_taggable' in the further process of the migration
- # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
- acts_as_ordered_taggable_on :topics
- has_many :topics_acts_as_taggable, -> { order("#{ActsAsTaggableOn::Tagging.table_name}.id") },
- class_name: 'ActsAsTaggableOn::Tag',
- through: :topic_taggings,
- source: :tag
-
has_many :project_topics, -> { order(:id) }, class_name: 'Projects::ProjectTopic'
has_many :topics, through: :project_topics, class_name: 'Projects::Topic'
- # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
- # TODO: remove 'topics' in the further process of the migration
- # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
- alias_method :topics_new, :topics
- def topics
- self.topics_acts_as_taggable + self.topics_new
- end
-
attr_accessor :old_path_with_namespace
attr_accessor :template_name
attr_writer :pipeline_status
@@ -159,11 +143,11 @@ class Project < ApplicationRecord
# Relations
belongs_to :pool_repository
belongs_to :creator, class_name: 'User'
- belongs_to :group, -> { where(type: 'Group') }, foreign_key: 'namespace_id'
+ belongs_to :group, -> { where(type: Group.sti_name) }, foreign_key: 'namespace_id'
belongs_to :namespace
# Sync deletion via DB Trigger to ensure we do not have
# a project without a project_namespace (or vice-versa)
- belongs_to :project_namespace, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
+ belongs_to :project_namespace, autosave: true, class_name: 'Namespaces::ProjectNamespace', foreign_key: 'project_namespace_id', inverse_of: :project
alias_method :parent, :namespace
alias_attribute :parent_id, :namespace_id
@@ -233,6 +217,7 @@ class Project < ApplicationRecord
has_one :import_state, autosave: true, class_name: 'ProjectImportState', inverse_of: :project
has_one :import_export_upload, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
has_many :export_jobs, class_name: 'ProjectExportJob'
+ has_many :bulk_import_exports, class_name: 'BulkImports::Export', inverse_of: :project
has_one :project_repository, inverse_of: :project
has_one :tracing_setting, class_name: 'ProjectTracingSetting'
has_one :incident_management_setting, inverse_of: :project, class_name: 'IncidentManagement::ProjectIncidentManagementSetting'
@@ -652,15 +637,8 @@ class Project < ApplicationRecord
scope :with_topic, ->(topic_name) do
topic = Projects::Topic.find_by_name(topic_name)
- acts_as_taggable_on_topic = ActsAsTaggableOn::Tag.find_by_name(topic_name)
- return none unless topic || acts_as_taggable_on_topic
-
- relations = []
- relations << where(id: topic.project_topics.select(:project_id)) if topic
- relations << where(id: acts_as_taggable_on_topic.taggings.select(:taggable_id)) if acts_as_taggable_on_topic
-
- Project.from_union(relations)
+ topic ? where(id: topic.project_topics.select(:project_id)) : none
end
enum auto_cancel_pending_pipelines: { disabled: 0, enabled: 1 }
@@ -678,7 +656,7 @@ class Project < ApplicationRecord
mount_uploader :bfg_object_map, AttachmentUploader
def self.with_api_entity_associations
- preload(:project_feature, :route, :topics, :topics_acts_as_taggable, :group, :timelogs, namespace: [:route, :owner])
+ preload(:project_feature, :route, :topics, :group, :timelogs, namespace: [:route, :owner])
end
def self.with_web_entity_associations
@@ -851,7 +829,7 @@ class Project < ApplicationRecord
end
def group_ids
- joins(:namespace).where(namespaces: { type: 'Group' }).select(:namespace_id)
+ joins(:namespace).where(namespaces: { type: Group.sti_name }).select(:namespace_id)
end
# Returns ids of projects with issuables available for given user
@@ -1200,7 +1178,7 @@ class Project < ApplicationRecord
end
def import?
- external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import?
+ external_import? || forked? || gitlab_project_import? || jira_import? || bare_repository_import? || gitlab_project_migration?
end
def external_import?
@@ -1223,6 +1201,10 @@ class Project < ApplicationRecord
import_type == 'gitlab_project'
end
+ def gitlab_project_migration?
+ import_type == 'gitlab_project_migration'
+ end
+
def gitea_import?
import_type == 'gitea'
end
@@ -1327,11 +1309,21 @@ class Project < ApplicationRecord
def changing_shared_runners_enabled_is_allowed
return unless new_record? || changes.has_key?(:shared_runners_enabled)
- if shared_runners_enabled && group && group.shared_runners_setting == 'disabled_and_unoverridable'
+ if shared_runners_setting_conflicting_with_group?
errors.add(:shared_runners_enabled, _('cannot be enabled because parent group does not allow it'))
end
end
+ def shared_runners_setting_conflicting_with_group?
+ shared_runners_enabled && group&.shared_runners_setting == Namespace::SR_DISABLED_AND_UNOVERRIDABLE
+ end
+
+ def reconcile_shared_runners_setting!
+ if shared_runners_setting_conflicting_with_group?
+ self.shared_runners_enabled = false
+ end
+ end
+
def to_param
if persisted? && errors.include?(:path)
path_was
@@ -1814,7 +1806,7 @@ class Project < ApplicationRecord
def open_issues_count(current_user = nil)
return Projects::OpenIssuesCountService.new(self, current_user).count unless current_user.nil?
- BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
+ BatchLoader.for(self).batch do |projects, loader|
issues_count_per_project = ::Projects::BatchOpenIssuesCountService.new(projects).refresh_cache_and_retrieve_data
issues_count_per_project.each do |project, count|
@@ -2279,7 +2271,7 @@ class Project < ApplicationRecord
# rubocop: disable CodeReuse/ServiceClass
def forks_count
- BatchLoader.for(self).batch(replace_methods: false) do |projects, loader|
+ BatchLoader.for(self).batch do |projects, loader|
fork_count_per_project = ::Projects::BatchForksCountService.new(projects).refresh_cache_and_retrieve_data
fork_count_per_project.each do |project, count|
@@ -2418,7 +2410,7 @@ class Project < ApplicationRecord
end
def mark_primary_write_location
- ::Gitlab::Database::LoadBalancing::Sticking.mark_primary_write_location(:project, self.id)
+ self.class.sticking.mark_primary_write_location(:project, self.id)
end
def toggle_ci_cd_settings!(settings_attribute)
@@ -2677,10 +2669,6 @@ class Project < ApplicationRecord
ProjectStatistics.increment_statistic(self, statistic, delta)
end
- def merge_requests_author_approval
- !!read_attribute(:merge_requests_author_approval)
- end
-
def ci_forward_deployment_enabled?
return false unless ci_cd_settings
@@ -2749,15 +2737,9 @@ class Project < ApplicationRecord
@topic_list = @topic_list.split(',') if @topic_list.instance_of?(String)
@topic_list = @topic_list.map(&:strip).uniq.reject(&:empty?)
- if @topic_list != self.topic_list || self.topics_acts_as_taggable.any?
- self.topics_new.delete_all
+ if @topic_list != self.topic_list
+ self.topics.delete_all
self.topics = @topic_list.map { |topic| Projects::Topic.find_or_create_by(name: topic) }
-
- # Remove old topics (ActsAsTaggableOn::Tag)
- # Required during the `ActsAsTaggableOn::Tag -> Topic` migration
- # TODO: remove in the further process of the migration
- # https://gitlab.com/gitlab-org/gitlab/-/issues/335946
- self.topic_taggings.clear
end
@topic_list = nil
@@ -2927,6 +2909,15 @@ class Project < ApplicationRecord
def online_runners_with_tags
@online_runners_with_tags ||= active_runners.with_tags.online
end
+
+ def ensure_project_namespace_in_sync
+ if changes.keys & [:name, :path, :namespace_id, :visibility_level] && project_namespace.present?
+ project_namespace.name = name
+ project_namespace.path = path
+ project_namespace.parent = namespace
+ project_namespace.visibility_level = visibility_level
+ end
+ end
end
Project.prepend_mod_with('Project')
diff --git a/app/models/project_setting.rb b/app/models/project_setting.rb
index b2559636f3..24d892290a 100644
--- a/app/models/project_setting.rb
+++ b/app/models/project_setting.rb
@@ -1,10 +1,6 @@
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
- include IgnorableColumns
-
- ignore_column :allow_editing_commit_messages, remove_with: '14.4', remove_after: '2021-09-10'
-
belongs_to :project, inverse_of: :project_setting
enum squash_option: {
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 387732cf15..99cec647a9 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -31,7 +31,6 @@ class ProjectStatistics < ApplicationRecord
scope :for_project_ids, ->(project_ids) { where(project_id: project_ids) }
scope :for_namespaces, -> (namespaces) { where(namespace: namespaces) }
- scope :with_any_ci_minutes_used, -> { where.not(shared_runners_seconds: 0) }
def total_repository_size
repository_size + lfs_objects_size
@@ -70,7 +69,7 @@ class ProjectStatistics < ApplicationRecord
end
def update_lfs_objects_size
- self.lfs_objects_size = project.lfs_objects.sum(:size)
+ self.lfs_objects_size = LfsObject.joins(:lfs_objects_projects).where(lfs_objects_projects: { project_id: project.id }).sum(:size)
end
def update_uploads_size
diff --git a/app/models/projects/project_topic.rb b/app/models/projects/project_topic.rb
index d4b456ef48..7021a48646 100644
--- a/app/models/projects/project_topic.rb
+++ b/app/models/projects/project_topic.rb
@@ -3,6 +3,6 @@
module Projects
class ProjectTopic < ApplicationRecord
belongs_to :project
- belongs_to :topic
+ belongs_to :topic, counter_cache: :total_projects_count
end
end
diff --git a/app/models/projects/topic.rb b/app/models/projects/topic.rb
index a17aa550ed..f3352ecc5e 100644
--- a/app/models/projects/topic.rb
+++ b/app/models/projects/topic.rb
@@ -1,10 +1,30 @@
# frozen_string_literal: true
+require 'carrierwave/orm/activerecord'
+
module Projects
class Topic < ApplicationRecord
+ include Avatarable
+ include Gitlab::SQL::Pattern
+
validates :name, presence: true, uniqueness: true, length: { maximum: 255 }
+ validates :description, length: { maximum: 1024 }
has_many :project_topics, class_name: 'Projects::ProjectTopic'
has_many :projects, through: :project_topics
+
+ scope :order_by_total_projects_count, -> { order(total_projects_count: :desc).order(id: :asc) }
+ scope :reorder_by_similarity, -> (search) do
+ order_expression = Gitlab::Database::SimilarityScore.build_expression(search: search, rules: [
+ { column: arel_table['name'] }
+ ])
+ reorder(order_expression.desc, arel_table['total_projects_count'].desc, arel_table['id'])
+ end
+
+ class << self
+ def search(query)
+ fuzzy_search(query, [:name])
+ end
+ end
end
end
diff --git a/app/models/protected_branch.rb b/app/models/protected_branch.rb
index 3d32144e0f..96002c8668 100644
--- a/app/models/protected_branch.rb
+++ b/app/models/protected_branch.rb
@@ -10,6 +10,8 @@ class ProtectedBranch < ApplicationRecord
scope :allowing_force_push,
-> { where(allow_force_push: true) }
+ scope :get_ids_by_name, -> (name) { where(name: name).pluck(:id) }
+
protected_ref_access_levels :merge, :push
def self.protected_ref_accessible_to?(ref, user, project:, action:, protected_refs: nil)
@@ -25,12 +27,17 @@ class ProtectedBranch < ApplicationRecord
# Check if branch name is marked as protected in the system
def self.protected?(project, ref_name)
return true if project.empty_repo? && project.default_branch_protected?
+ return false if ref_name.blank?
- Rails.cache.fetch("protected_ref-#{ref_name}-#{project.cache_key}") do
+ Rails.cache.fetch(protected_ref_cache_key(project, ref_name)) do
self.matching(ref_name, protected_refs: protected_refs(project)).present?
end
end
+ def self.protected_ref_cache_key(project, ref_name)
+ "protected_ref-#{project.cache_key}-#{Digest::SHA1.hexdigest(ref_name)}"
+ end
+
def self.allow_force_push?(project, ref_name)
project.protected_branches.allowing_force_push.matching(ref_name).any?
end
diff --git a/app/models/release.rb b/app/models/release.rb
index 0dd71c6ebf..eac6346cc6 100644
--- a/app/models/release.rb
+++ b/app/models/release.rb
@@ -33,6 +33,7 @@ class Release < ApplicationRecord
includes(:author, :evidences, :milestones, :links, :sorted_links,
project: [:project_feature, :route, { namespace: :route }])
}
+ scope :with_milestones, -> { joins(:milestone_releases) }
scope :recent, -> { sorted.limit(MAX_NUMBER_TO_DISPLAY) }
scope :without_evidence, -> { left_joins(:evidences).where(::Releases::Evidence.arel_table[:id].eq(nil)) }
scope :released_within_2hrs, -> { where(released_at: Time.zone.now - 1.hour..Time.zone.now + 1.hour) }
diff --git a/app/models/repository.rb b/app/models/repository.rb
index f20b306c80..119d874a6e 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -732,7 +732,7 @@ class Repository
end
def tags_sorted_by(value)
- return raw_repository.tags(sort_by: value) if Feature.enabled?(:gitaly_tags_finder, project, default_enabled: :yaml)
+ return raw_repository.tags(sort_by: value) if Feature.enabled?(:tags_finder_gitaly, project, default_enabled: :yaml)
tags_ruby_sort(value)
end
@@ -1054,10 +1054,10 @@ class Repository
end
def squash(user, merge_request, message)
- raw.squash(user, merge_request.id, start_sha: merge_request.diff_start_sha,
- end_sha: merge_request.diff_head_sha,
- author: merge_request.author,
- message: message)
+ raw.squash(user, start_sha: merge_request.diff_start_sha,
+ end_sha: merge_request.diff_head_sha,
+ author: merge_request.author,
+ message: message)
end
def submodule_links
diff --git a/app/models/upload.rb b/app/models/upload.rb
index 0a4acdfc7e..c1a3df8245 100644
--- a/app/models/upload.rb
+++ b/app/models/upload.rb
@@ -18,6 +18,8 @@ class Upload < ApplicationRecord
before_save :calculate_checksum!, if: :foreground_checksummable?
after_commit :schedule_checksum, if: :needs_checksum?
+ after_commit :update_project_statistics, on: [:create, :destroy], if: :project?
+
# as the FileUploader is not mounted, the default CarrierWave ActiveRecord
# hooks are not executed and the file will not be deleted
after_destroy :delete_file!, if: -> { uploader_class <= FileUploader }
@@ -67,7 +69,7 @@ class Upload < ApplicationRecord
self.checksum = nil
return unless needs_checksum?
- self.checksum = self.class.hexdigest(absolute_path)
+ self.checksum = self.class.sha256_hexdigest(absolute_path)
end
# Initialize the associated Uploader class with current model
@@ -161,6 +163,14 @@ class Upload < ApplicationRecord
def mount_point
super&.to_sym
end
+
+ def project?
+ model_type == "Project"
+ end
+
+ def update_project_statistics
+ ProjectCacheWorker.perform_async(model_id, [], [:uploads_size])
+ end
end
Upload.prepend_mod_with('Upload')
diff --git a/app/models/user.rb b/app/models/user.rb
index 71db475816..0e19e6e4a7 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -112,7 +112,14 @@ class User < ApplicationRecord
#
# Namespace for personal projects
- has_one :namespace, -> { where(type: nil) }, dependent: :destroy, foreign_key: :owner_id, inverse_of: :owner, autosave: true # rubocop:disable Cop/ActiveRecordDependent
+ # TODO: change to `:namespace, -> { where(type: Namespaces::UserNamespace.sti_name}, class_name: 'Namespaces::UserNamespace'...`
+ # when working on issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
+ has_one :namespace,
+ -> { where(type: [nil, Namespaces::UserNamespace.sti_name]) },
+ dependent: :destroy, # rubocop:disable Cop/ActiveRecordDependent
+ foreign_key: :owner_id,
+ inverse_of: :owner,
+ autosave: true # rubocop:disable Cop/ActiveRecordDependent
# Profile
has_many :keys, -> { regular_keys }, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
@@ -229,9 +236,9 @@ class User < ApplicationRecord
validates :first_name, length: { maximum: 127 }
validates :last_name, length: { maximum: 127 }
validates :email, confirmation: true
- validates :notification_email, devise_email: true, allow_blank: true, if: ->(user) { user.notification_email != user.email }
+ validates :notification_email, devise_email: true, allow_blank: true
validates :public_email, uniqueness: true, devise_email: true, allow_blank: true
- validates :commit_email, devise_email: true, allow_blank: true, if: ->(user) { user.commit_email != user.email && user.commit_email != Gitlab::PrivateCommitEmail::TOKEN }
+ validates :commit_email, devise_email: true, allow_blank: true, unless: ->(user) { user.commit_email == Gitlab::PrivateCommitEmail::TOKEN }
validates :projects_limit,
presence: true,
numericality: { greater_than_or_equal_to: 0, less_than_or_equal_to: Gitlab::Database::MAX_INT_VALUE }
@@ -316,6 +323,7 @@ class User < ApplicationRecord
delegate :webauthn_xid, :webauthn_xid=, to: :user_detail, allow_nil: true
delegate :pronouns, :pronouns=, to: :user_detail, allow_nil: true
delegate :pronunciation, :pronunciation=, to: :user_detail, allow_nil: true
+ delegate :registration_objective, :registration_objective=, to: :user_detail, allow_nil: true
accepts_nested_attributes_for :user_preference, update_only: true
accepts_nested_attributes_for :user_detail, update_only: true
@@ -449,11 +457,12 @@ class User < ApplicationRecord
scope :dormant, -> { active.where('last_activity_on <= ?', MINIMUM_INACTIVE_DAYS.day.ago.to_date) }
scope :with_no_activity, -> { active.where(last_activity_on: nil) }
scope :by_provider_and_extern_uid, ->(provider, extern_uid) { joins(:identities).merge(Identity.with_extern_uid(provider, extern_uid)) }
+ scope :get_ids_by_username, -> (username) { where(username: username).pluck(:id) }
def preferred_language
read_attribute('preferred_language') ||
I18n.default_locale.to_s.presence_in(Gitlab::I18n.available_locales) ||
- 'en'
+ default_preferred_language
end
def active_for_authentication?
@@ -728,7 +737,7 @@ class User < ApplicationRecord
end
def find_by_full_path(path, follow_redirects: false)
- namespace = Namespace.for_user.find_by_full_path(path, follow_redirects: follow_redirects)
+ namespace = Namespace.user_namespaces.find_by_full_path(path, follow_redirects: follow_redirects)
namespace&.owner
end
@@ -1434,7 +1443,10 @@ class User < ApplicationRecord
namespace.path = username if username_changed?
namespace.name = name if name_changed?
else
- namespace = build_namespace(path: username, name: name)
+ # TODO: we should no longer need the `type` parameter once we can make the
+ # the `has_one :namespace` association use the correct class.
+ # issue https://gitlab.com/gitlab-org/gitlab/-/issues/341070
+ namespace = build_namespace(path: username, name: name, type: ::Namespaces::UserNamespace.sti_name)
namespace.build_namespace_settings
end
end
@@ -2003,6 +2015,11 @@ class User < ApplicationRecord
private
+ # To enable JiHu repository to modify the default language options
+ def default_preferred_language
+ 'en'
+ end
+
def notification_email_verified
return if notification_email.blank? || temp_oauth_email?
@@ -2094,10 +2111,14 @@ class User < ApplicationRecord
errors.add(:email, error) if error
end
+ def signup_email_invalid_message
+ _('is not allowed for sign-up.')
+ end
+
def check_username_format
return if username.blank? || Mime::EXTENSION_LOOKUP.keys.none? { |type| username.end_with?(".#{type}") }
- errors.add(:username, _('ending with a file extension is not allowed.'))
+ errors.add(:username, _('ending with a reserved file extension is not allowed.'))
end
def groups_with_developer_maintainer_project_access
diff --git a/app/models/user_callout.rb b/app/models/user_callout.rb
index 04bc29755f..b990aedd4f 100644
--- a/app/models/user_callout.rb
+++ b/app/models/user_callout.rb
@@ -36,7 +36,8 @@ class UserCallout < ApplicationRecord
trial_status_reminder_d3: 35, # EE-only
security_configuration_devops_alert: 36, # EE-only
profile_personal_access_token_expiry: 37, # EE-only
- terraform_notification_dismissed: 38
+ terraform_notification_dismissed: 38,
+ security_newsletter_callout: 39
}
validates :feature_name,
diff --git a/app/models/user_detail.rb b/app/models/user_detail.rb
index c41cff6786..6b0ed89c68 100644
--- a/app/models/user_detail.rb
+++ b/app/models/user_detail.rb
@@ -3,7 +3,10 @@
class UserDetail < ApplicationRecord
extend ::Gitlab::Utils::Override
include IgnorableColumns
- ignore_columns %i[bio_html cached_markdown_version], remove_with: '13.6', remove_after: '2021-10-22'
+
+ ignore_columns %i[bio_html cached_markdown_version], remove_with: '14.5', remove_after: '2021-10-22'
+
+ REGISTRATION_OBJECTIVE_PAIRS = { basics: 0, move_repository: 1, code_storage: 2, exploring: 3, ci: 4, other: 5, joining_team: 6 }.freeze
belongs_to :user
@@ -14,6 +17,8 @@ class UserDetail < ApplicationRecord
before_save :prevent_nil_bio
+ enum registration_objective: REGISTRATION_OBJECTIVE_PAIRS, _suffix: true
+
private
def prevent_nil_bio
diff --git a/app/models/user_highest_role.rb b/app/models/user_highest_role.rb
index 4853fc3d24..dd5c85a5a8 100644
--- a/app/models/user_highest_role.rb
+++ b/app/models/user_highest_role.rb
@@ -3,7 +3,13 @@
class UserHighestRole < ApplicationRecord
belongs_to :user, optional: false
- validates :highest_access_level, allow_nil: true, inclusion: { in: Gitlab::Access.all_values }
+ validates :highest_access_level, allow_nil: true, inclusion: { in: ->(_) { self.allowed_values } }
scope :with_highest_access_level, -> (highest_access_level) { where(highest_access_level: highest_access_level) }
+
+ def self.allowed_values
+ Gitlab::Access.all_values
+ end
end
+
+UserHighestRole.prepend_mod
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 337ae7125f..7687430cfd 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -23,7 +23,6 @@ class UserPreference < ApplicationRecord
ignore_columns :experience_level, remove_with: '14.10', remove_after: '2021-03-22'
default_value_for :tab_width, value: Gitlab::TabWidth::DEFAULT, allows_nil: false
- default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
default_value_for :time_display_relative, value: true, allows_nil: false
default_value_for :time_format_in_24h, value: false, allows_nil: false
default_value_for :render_whitespace_in_code, value: false, allows_nil: false
diff --git a/app/models/users/credit_card_validation.rb b/app/models/users/credit_card_validation.rb
index 5e255acd88..a4cc43d1f1 100644
--- a/app/models/users/credit_card_validation.rb
+++ b/app/models/users/credit_card_validation.rb
@@ -7,5 +7,18 @@ module Users
self.table_name = 'user_credit_card_validations'
belongs_to :user
+
+ validates :holder_name, length: { maximum: 26 }
+ validates :last_digits, allow_nil: true, numericality: {
+ greater_than_or_equal_to: 0, less_than_or_equal_to: 9999
+ }
+
+ def similar_records
+ self.class.where(
+ expiration_date: expiration_date,
+ last_digits: last_digits,
+ holder_name: holder_name
+ ).order(credit_card_validated_at: :desc).includes(:user)
+ end
end
end
diff --git a/app/policies/ci/resource_group_policy.rb b/app/policies/ci/resource_group_policy.rb
new file mode 100644
index 0000000000..ef384265b1
--- /dev/null
+++ b/app/policies/ci/resource_group_policy.rb
@@ -0,0 +1,7 @@
+# frozen_string_literal: true
+
+module Ci
+ class ResourceGroupPolicy < BasePolicy
+ delegate { @subject.project }
+ end
+end
diff --git a/app/policies/clusters/agent_policy.rb b/app/policies/clusters/agent_policy.rb
new file mode 100644
index 0000000000..25e78c8480
--- /dev/null
+++ b/app/policies/clusters/agent_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentPolicy < BasePolicy
+ alias_method :cluster_agent, :subject
+
+ delegate { cluster_agent.project }
+ end
+end
diff --git a/app/policies/clusters/agent_token_policy.rb b/app/policies/clusters/agent_token_policy.rb
new file mode 100644
index 0000000000..e876ecfac2
--- /dev/null
+++ b/app/policies/clusters/agent_token_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Clusters
+ class AgentTokenPolicy < BasePolicy
+ alias_method :token, :subject
+
+ delegate { token.agent }
+ end
+end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 7abffd2c35..64395f69c4 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -134,6 +134,8 @@ class GroupPolicy < BasePolicy
enable :create_package
enable :create_package_settings
enable :developer_access
+ enable :admin_organization
+ enable :admin_contact
end
rule { reporter }.policy do
@@ -147,7 +149,6 @@ class GroupPolicy < BasePolicy
enable :read_prometheus
enable :read_package
enable :read_package_settings
- enable :admin_organization
end
rule { maintainer }.policy do
@@ -162,7 +163,6 @@ class GroupPolicy < BasePolicy
enable :admin_cluster
enable :read_deploy_token
enable :create_jira_connect_subscription
- enable :update_runners_registration_token
enable :maintainer_access
end
@@ -179,6 +179,7 @@ class GroupPolicy < BasePolicy
enable :update_default_branch_protection
enable :create_deploy_token
enable :destroy_deploy_token
+ enable :update_runners_registration_token
enable :owner_access
end
diff --git a/app/policies/list_policy.rb b/app/policies/list_policy.rb
new file mode 100644
index 0000000000..9784574654
--- /dev/null
+++ b/app/policies/list_policy.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class ListPolicy < BasePolicy # rubocop:disable Gitlab/NamespacedClass
+ delegate { @subject.board.resource_parent }
+end
diff --git a/app/policies/namespace_policy.rb b/app/policies/namespace_policy.rb
index dcbeda9f5d..0cf1bcb973 100644
--- a/app/policies/namespace_policy.rb
+++ b/app/policies/namespace_policy.rb
@@ -1,26 +1,9 @@
# frozen_string_literal: true
-class NamespacePolicy < BasePolicy
- rule { anonymous }.prevent_all
-
- condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
- condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
- condition(:owner) { @subject.owner == @user }
-
- rule { owner | admin }.policy do
- enable :owner_access
- enable :create_projects
- enable :admin_namespace
- enable :read_namespace
- enable :read_statistics
- enable :create_jira_connect_subscription
- enable :create_package_settings
- enable :read_package_settings
- end
-
- rule { personal_project & ~can_create_personal_project }.prevent :create_projects
-
- rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
+class NamespacePolicy < ::Namespaces::UserNamespacePolicy
+ # NamespacePolicy has been traditionally for user namespaces.
+ # So these policies have been moved into Namespaces::UserNamespacePolicy.
+ # Once the user namespace conversion is complete, we can look at
+ # either removing this file or locating common namespace policy items
+ # here.
end
-
-NamespacePolicy.prepend_mod_with('NamespacePolicy')
diff --git a/app/policies/namespaces/project_namespace_policy.rb b/app/policies/namespaces/project_namespace_policy.rb
new file mode 100644
index 0000000000..bc08a7a45e
--- /dev/null
+++ b/app/policies/namespaces/project_namespace_policy.rb
@@ -0,0 +1,9 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class ProjectNamespacePolicy < BasePolicy
+ # For now users are not granted any permissions on project namespace
+ # as it's completely hidden to them. When we start using project
+ # namespaces in queries, we will have to extend this policy.
+ end
+end
diff --git a/app/policies/namespaces/user_namespace_policy.rb b/app/policies/namespaces/user_namespace_policy.rb
new file mode 100644
index 0000000000..f8b285e531
--- /dev/null
+++ b/app/policies/namespaces/user_namespace_policy.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module Namespaces
+ class UserNamespacePolicy < BasePolicy
+ rule { anonymous }.prevent_all
+
+ condition(:personal_project, scope: :subject) { @subject.kind == 'user' }
+ condition(:can_create_personal_project, scope: :user) { @user.can_create_project? }
+ condition(:owner) { @subject.owner == @user }
+
+ rule { owner | admin }.policy do
+ enable :owner_access
+ enable :create_projects
+ enable :admin_namespace
+ enable :read_namespace
+ enable :read_statistics
+ enable :create_jira_connect_subscription
+ enable :create_package_settings
+ enable :read_package_settings
+ end
+
+ rule { personal_project & ~can_create_personal_project }.prevent :create_projects
+
+ rule { (owner | admin) & can?(:create_projects) }.enable :transfer_projects
+ end
+end
+
+Namespaces::UserNamespacePolicy.prepend_mod_with('Namespaces::UserNamespacePolicy')
diff --git a/app/policies/project_policy.rb b/app/policies/project_policy.rb
index b40bfbf5a7..87573c9ad1 100644
--- a/app/policies/project_policy.rb
+++ b/app/policies/project_policy.rb
@@ -358,6 +358,8 @@ class ProjectPolicy < BasePolicy
enable :update_commit_status
enable :create_build
enable :update_build
+ enable :read_resource_group
+ enable :update_resource_group
enable :create_merge_request_from
enable :create_wiki
enable :push_code
@@ -437,6 +439,7 @@ class ProjectPolicy < BasePolicy
enable :destroy_freeze_period
enable :admin_feature_flags_client
enable :update_runners_registration_token
+ enable :manage_project_google_cloud
end
rule { public_project & metrics_dashboard_allowed }.policy do
diff --git a/app/presenters/README.md b/app/presenters/README.md
index 62aec4fc8a..dfd1818f97 100644
--- a/app/presenters/README.md
+++ b/app/presenters/README.md
@@ -66,14 +66,15 @@ we gain the following benefits:
### Presenter definition
-Every presenter should inherit from `Gitlab::View::Presenter::Simple`, which
-provides a `.presents` the method which allows you to define an accessor for the
+If you need a presenter class that has only necessary interfaces for the view-related context,
+inherit from `Gitlab::View::Presenter::Simple`.
+It provides a `.presents` the method which allows you to define an accessor for the
presented object. It also includes common helpers like `Gitlab::Routing` and
`Gitlab::Allowable`.
```ruby
class LabelPresenter < Gitlab::View::Presenter::Simple
- presents :label
+ presents ::Label, as: :label
def text_color
label.color.to_s
@@ -85,13 +86,14 @@ class LabelPresenter < Gitlab::View::Presenter::Simple
end
```
-In some cases, it can be more practical to transparently delegate all missing
-method calls to the presented object, in these cases, you can make your
-presenter inherit from `Gitlab::View::Presenter::Delegated`:
+If you need a presenter class that delegates missing method calls to the presented object,
+inherit from `Gitlab::View::Presenter::Delegated`.
+This is more like an "extension" in the sense that the produced object is going to have
+all of interfaces of the presented object **AND** all of the interfaces in the presenter class:
```ruby
class LabelPresenter < Gitlab::View::Presenter::Delegated
- presents :label
+ presents ::Label, as: :label
def text_color
# color is delegated to label
@@ -152,3 +154,82 @@ You can also present the model in the view:
%div{ class: label.text_color }
= render partial: label, label: label
```
+
+### Validate accidental overrides
+
+We use presenters in many places, such as Controller, Haml, GraphQL/Rest API,
+it's very handy to extend the core/backend logic of Active Record models,
+however, there is a risk that it accidentally overrides important logic.
+
+For example, [this production incident](https://gitlab.com/gitlab-com/gl-infra/production/-/issues/5498)
+was caused by [including `ActionView::Helpers::UrlHelper` in a presenter](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537/diffs#4b581cff00ef3cc9780efd23682af383de302e7d_3_3).
+The `tag` accesor in `Ci::Build` was accidentally overridden by `ActionView::Helpers::TagHelper#tag`,
+and as a conseuqence, a wrong `tag` value was persited into database.
+
+Starting from GitLab 14.4, we validate the presenters (specifically all of the subclasses of `Gitlab::View::Presenter::Delegated`)
+that they do not accidentally override core/backend logic. In such case, a pipeline in merge requests fails with an error message,
+here is an example:
+
+```plaintext
+We've detected that a presetner is overriding a specific method(s) on a subject model.
+There is a risk that it accidentally modifies the backend/core logic that leads to production incident.
+Please follow https://gitlab.com/gitlab-org/gitlab/-/blob/master/app/presenters/README.md#validate-accidental-overrides
+to resolve this error with caution.
+
+Here are the conflict details.
+
+- Ci::PipelinePresenter#tag is overriding Ci::Pipeline#tag. delegator_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/actionview-6.1.3.2/lib/action_view/helpers/tag_helper.rb:271 original_location: /devkitkat/services/rails/cache/ruby/2.7.0/gems/activemodel-6.1.3.2/lib/active_model/attribute_methods.rb:254
+```
+
+Here are the potential solutions:
+
+- If the conflict happens on an instance method in the presenter:
+ - If you intend to override the core/backend logic, define `delegator_override ` on top of the conflicted method.
+ This explicitly adds the method to an allowlist.
+ - If you do NOT intend to override the core/backend logic, rename the method name in the presenter.
+- If the conflict happens on an included module in the presenter, remove the module from the presenter and find a workaround.
+
+### How to use the `Gitlab::Utils::DelegatorOverride` validator
+
+If a presenter class inhertis from `Gitlab::View::Presenter::Delegated`,
+you should define what object class is presented:
+
+```ruby
+class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
+ presents ::WebHookLog, as: :web_hook_log # This defines that the presenter presents `WebHookLog` Active Record model.
+```
+
+These presenters are validated not to accidentaly override the methods in the presented object.
+You can run the validation locally with:
+
+```shell
+bundle exec rake lint:static_verification
+```
+
+To add a method to an allowlist, use `delegator_override`. For example:
+
+```ruby
+class VulnerabilityPresenter < Gitlab::View::Presenter::Delegated
+ presents ::Vulnerability, as: :vulnerability
+
+ delegator_override :description # This adds the `description` method to an allowlist that the override is intentional.
+ def description
+ vulnerability.description || finding.description
+ end
+```
+
+To add methods of a module to an allowlist, use `delegator_override_with`. For example:
+
+```ruby
+module Ci
+ class PipelinePresenter < Gitlab::View::Presenter::Delegated
+ include Gitlab::Utils::StrongMemoize
+ include ActionView::Helpers::UrlHelper
+
+ delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+ delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
+```
+
+Keep in mind that if you use `delegator_override_with`,
+there is a high chance that you're doing **something wrong**.
+Read the [Validate Accidental Overrides](#validate-accidental-overrides) for more information.
diff --git a/app/presenters/alert_management/alert_presenter.rb b/app/presenters/alert_management/alert_presenter.rb
index c6c6fe837a..86fe985927 100644
--- a/app/presenters/alert_management/alert_presenter.rb
+++ b/app/presenters/alert_management/alert_presenter.rb
@@ -2,10 +2,12 @@
module AlertManagement
class AlertPresenter < Gitlab::View::Presenter::Delegated
- include Gitlab::Utils::StrongMemoize
include IncidentManagement::Settings
include ActionView::Helpers::UrlHelper
+ presents ::AlertManagement::Alert
+ delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
MARKDOWN_LINE_BREAK = " \n"
HORIZONTAL_LINE = "\n\n---\n\n"
@@ -30,6 +32,7 @@ module AlertManagement
started_at&.strftime('%d %B %Y, %-l:%M%p (%Z)')
end
+ delegator_override :details_url
def details_url
details_project_alert_management_url(project, alert.iid)
end
@@ -65,6 +68,7 @@ module AlertManagement
private
attr_reader :alert, :project
+
delegate :alert_markdown, :full_query, to: :parsed_payload
def issue_summary_markdown
diff --git a/app/presenters/award_emoji_presenter.rb b/app/presenters/award_emoji_presenter.rb
index 98713855d3..8a7b58e0ab 100644
--- a/app/presenters/award_emoji_presenter.rb
+++ b/app/presenters/award_emoji_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class AwardEmojiPresenter < Gitlab::View::Presenter::Delegated
- presents :award_emoji
+ presents ::AwardEmoji, as: :award_emoji
def description
as_emoji['description']
diff --git a/app/presenters/blob_presenter.rb b/app/presenters/blob_presenter.rb
index ecc16e2840..c198859aa4 100644
--- a/app/presenters/blob_presenter.rb
+++ b/app/presenters/blob_presenter.rb
@@ -7,7 +7,7 @@ class BlobPresenter < Gitlab::View::Presenter::Delegated
include TreeHelper
include ChecksCollaboration
- presents :blob
+ presents ::Blob, as: :blob
def highlight(to: nil, plain: nil)
load_all_blob_data
diff --git a/app/presenters/blobs/unfold_presenter.rb b/app/presenters/blobs/unfold_presenter.rb
index 487c6fe075..b921b5bf67 100644
--- a/app/presenters/blobs/unfold_presenter.rb
+++ b/app/presenters/blobs/unfold_presenter.rb
@@ -6,6 +6,8 @@ module Blobs
include ActiveModel::AttributeAssignment
include Gitlab::Utils::StrongMemoize
+ presents ::Blob
+
attribute :full, :boolean, default: false
attribute :since, :integer, default: 1
attribute :to, :integer, default: 1
diff --git a/app/presenters/board_presenter.rb b/app/presenters/board_presenter.rb
index d7cecd44dd..bb3e96b8fa 100644
--- a/app/presenters/board_presenter.rb
+++ b/app/presenters/board_presenter.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class BoardPresenter < Gitlab::View::Presenter::Delegated
- presents :board
+ presents ::Board, as: :board
end
diff --git a/app/presenters/ci/bridge_presenter.rb b/app/presenters/ci/bridge_presenter.rb
index 724e10c26c..a62d7cdbbd 100644
--- a/app/presenters/ci/bridge_presenter.rb
+++ b/app/presenters/ci/bridge_presenter.rb
@@ -2,6 +2,9 @@
module Ci
class BridgePresenter < ProcessablePresenter
+ presents ::Ci::Bridge
+
+ delegator_override :detailed_status
def detailed_status
@detailed_status ||= subject.detailed_status(user)
end
diff --git a/app/presenters/ci/build_metadata_presenter.rb b/app/presenters/ci/build_metadata_presenter.rb
index 4871bb3a91..2f559adf77 100644
--- a/app/presenters/ci/build_metadata_presenter.rb
+++ b/app/presenters/ci/build_metadata_presenter.rb
@@ -9,8 +9,9 @@ module Ci
job_timeout_source: 'job'
}.freeze
- presents :metadata
+ presents ::Ci::BuildMetadata, as: :metadata
+ delegator_override :timeout_source
def timeout_source
return unless metadata.timeout_source?
diff --git a/app/presenters/ci/build_presenter.rb b/app/presenters/ci/build_presenter.rb
index 06ed6791bb..65e1c80085 100644
--- a/app/presenters/ci/build_presenter.rb
+++ b/app/presenters/ci/build_presenter.rb
@@ -2,6 +2,8 @@
module Ci
class BuildPresenter < ProcessablePresenter
+ presents ::Ci::Build
+
def erased_by_user?
# Build can be erased through API, therefore it does not have
# `erased_by` user assigned in that case.
diff --git a/app/presenters/ci/group_variable_presenter.rb b/app/presenters/ci/group_variable_presenter.rb
index 99011150c8..dea9a42b62 100644
--- a/app/presenters/ci/group_variable_presenter.rb
+++ b/app/presenters/ci/group_variable_presenter.rb
@@ -2,7 +2,7 @@
module Ci
class GroupVariablePresenter < Gitlab::View::Presenter::Delegated
- presents :variable
+ presents ::Ci::GroupVariable, as: :variable
def placeholder
'GROUP_VARIABLE'
diff --git a/app/presenters/ci/legacy_stage_presenter.rb b/app/presenters/ci/legacy_stage_presenter.rb
index d5c21baba2..c803abfab6 100644
--- a/app/presenters/ci/legacy_stage_presenter.rb
+++ b/app/presenters/ci/legacy_stage_presenter.rb
@@ -2,7 +2,7 @@
module Ci
class LegacyStagePresenter < Gitlab::View::Presenter::Delegated
- presents :legacy_stage
+ presents ::Ci::LegacyStage, as: :legacy_stage
def latest_ordered_statuses
preload_statuses(legacy_stage.statuses.latest_ordered)
diff --git a/app/presenters/ci/pipeline_presenter.rb b/app/presenters/ci/pipeline_presenter.rb
index 82f00f7469..e0cb899c9d 100644
--- a/app/presenters/ci/pipeline_presenter.rb
+++ b/app/presenters/ci/pipeline_presenter.rb
@@ -5,6 +5,9 @@ module Ci
include Gitlab::Utils::StrongMemoize
include ActionView::Helpers::UrlHelper
+ delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+ delegator_override_with ActionView::Helpers::TagHelper # TODO: Remove `ActionView::Helpers::UrlHelper` inclusion as it overrides `Ci::Pipeline#tag`
+
# We use a class method here instead of a constant, allowing EE to redefine
# the returned `Hash` more easily.
def self.failure_reasons
@@ -20,8 +23,9 @@ module Ci
user_blocked: 'The user who created this pipeline is blocked.' }
end
- presents :pipeline
+ presents ::Ci::Pipeline, as: :pipeline
+ delegator_override :failed_builds
def failed_builds
return [] unless can?(current_user, :read_build, pipeline)
@@ -30,6 +34,7 @@ module Ci
end
end
+ delegator_override :failure_reason
def failure_reason
return unless pipeline.failure_reason?
diff --git a/app/presenters/ci/processable_presenter.rb b/app/presenters/ci/processable_presenter.rb
index 5a8a664907..9f3a83a00f 100644
--- a/app/presenters/ci/processable_presenter.rb
+++ b/app/presenters/ci/processable_presenter.rb
@@ -2,5 +2,6 @@
module Ci
class ProcessablePresenter < CommitStatusPresenter
+ presents ::Ci::Processable
end
end
diff --git a/app/presenters/ci/runner_presenter.rb b/app/presenters/ci/runner_presenter.rb
index 273328afc5..ad889d444f 100644
--- a/app/presenters/ci/runner_presenter.rb
+++ b/app/presenters/ci/runner_presenter.rb
@@ -2,11 +2,14 @@
module Ci
class RunnerPresenter < Gitlab::View::Presenter::Delegated
- presents :runner
+ presents ::Ci::Runner, as: :runner
+ delegator_override :locked?
def locked?
read_attribute(:locked) && project_type?
end
+
+ delegator_override :locked
alias_method :locked, :locked?
end
end
diff --git a/app/presenters/ci/stage_presenter.rb b/app/presenters/ci/stage_presenter.rb
index 21bda86cde..bd5bf08abb 100644
--- a/app/presenters/ci/stage_presenter.rb
+++ b/app/presenters/ci/stage_presenter.rb
@@ -2,7 +2,7 @@
module Ci
class StagePresenter < Gitlab::View::Presenter::Delegated
- presents :stage
+ presents ::Ci::Stage, as: :stage
PRELOADED_RELATIONS = [:pipeline, :metadata, :tags, :job_artifacts_archive, :downstream_pipeline].freeze
diff --git a/app/presenters/ci/trigger_presenter.rb b/app/presenters/ci/trigger_presenter.rb
index 605c8f328a..5f0bd4b3a8 100644
--- a/app/presenters/ci/trigger_presenter.rb
+++ b/app/presenters/ci/trigger_presenter.rb
@@ -2,12 +2,13 @@
module Ci
class TriggerPresenter < Gitlab::View::Presenter::Delegated
- presents :trigger
+ presents ::Ci::Trigger, as: :trigger
def has_token_exposed?
can?(current_user, :admin_trigger, trigger)
end
+ delegator_override :token
def token
if has_token_exposed?
trigger.token
diff --git a/app/presenters/ci/variable_presenter.rb b/app/presenters/ci/variable_presenter.rb
index f027f3aa56..ec04dd5e9f 100644
--- a/app/presenters/ci/variable_presenter.rb
+++ b/app/presenters/ci/variable_presenter.rb
@@ -2,7 +2,7 @@
module Ci
class VariablePresenter < Gitlab::View::Presenter::Delegated
- presents :variable
+ presents ::Ci::Variable, as: :variable
def placeholder
'PROJECT_VARIABLE'
diff --git a/app/presenters/clusterable_presenter.rb b/app/presenters/clusterable_presenter.rb
index 03e26b9292..4b645510b5 100644
--- a/app/presenters/clusterable_presenter.rb
+++ b/app/presenters/clusterable_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ClusterablePresenter < Gitlab::View::Presenter::Delegated
- presents :clusterable
+ presents ::Project, ::Group, ::Clusters::Instance, as: :clusterable
def self.fabricate(clusterable, **attributes)
presenter_class = "#{clusterable.class.name}ClusterablePresenter".constantize
diff --git a/app/presenters/clusters/cluster_presenter.rb b/app/presenters/clusters/cluster_presenter.rb
index eb4bd8532a..ce060476cf 100644
--- a/app/presenters/clusters/cluster_presenter.rb
+++ b/app/presenters/clusters/cluster_presenter.rb
@@ -3,21 +3,10 @@
module Clusters
class ClusterPresenter < Gitlab::View::Presenter::Delegated
include ::Gitlab::Utils::StrongMemoize
- include ActionView::Helpers::SanitizeHelper
- include ActionView::Helpers::UrlHelper
- include IconsHelper
- presents :cluster
+ delegator_override_with ::Gitlab::Utils::StrongMemoize # TODO: Remove `::Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
- # We do not want to show the group path for clusters belonging to the
- # clusterable, only for the ancestor clusters.
- def item_link(clusterable_presenter, *html_options)
- if cluster.group_type? && clusterable != clusterable_presenter.subject
- contracted_group_name(cluster.group) + ' / ' + link_to_cluster
- else
- link_to_cluster(*html_options)
- end
- end
+ presents ::Clusters::Cluster, as: :cluster
def provider_label
if aws?
@@ -39,16 +28,6 @@ module Clusters
can?(current_user, :read_cluster, cluster)
end
- def cluster_type_description
- if cluster.project_type?
- s_("ClusterIntegration|Project cluster")
- elsif cluster.group_type?
- s_("ClusterIntegration|Group cluster")
- elsif cluster.instance_type?
- s_("ClusterIntegration|Instance cluster")
- end
- end
-
def show_path(params: {})
if cluster.project_type?
project_cluster_path(project, cluster, params)
@@ -107,7 +86,7 @@ module Clusters
private
def image_path(path)
- ActionController::Base.helpers.image_path(path)
+ ApplicationController.helpers.image_path(path)
end
# currently log explorer is only available in the scope of the project
@@ -127,20 +106,6 @@ module Clusters
cluster.project
end
end
-
- def contracted_group_name(group)
- sanitize(group.full_name)
- .sub(%r{\/.*\/}, "/ #{contracted_icon} /")
- .html_safe
- end
-
- def contracted_icon
- sprite_icon('ellipsis_h', size: 12, css_class: 'vertical-align-middle')
- end
-
- def link_to_cluster(html_options: {})
- link_to_if(can_read_cluster?, cluster.name, show_path, html_options)
- end
end
end
diff --git a/app/presenters/clusters/integration_presenter.rb b/app/presenters/clusters/integration_presenter.rb
index 57608be29b..f7be59f00f 100644
--- a/app/presenters/clusters/integration_presenter.rb
+++ b/app/presenters/clusters/integration_presenter.rb
@@ -2,7 +2,7 @@
module Clusters
class IntegrationPresenter < Gitlab::View::Presenter::Delegated
- presents :integration
+ presents ::Clusters::Integrations::Prometheus, ::Clusters::Integrations::ElasticStack, as: :integration
def application_type
integration.class.name.demodulize.underscore
diff --git a/app/presenters/commit_presenter.rb b/app/presenters/commit_presenter.rb
index c14dcab600..7df45ca03b 100644
--- a/app/presenters/commit_presenter.rb
+++ b/app/presenters/commit_presenter.rb
@@ -3,7 +3,7 @@
class CommitPresenter < Gitlab::View::Presenter::Delegated
include GlobalID::Identification
- presents :commit
+ presents ::Commit, as: :commit
def status_for(ref)
return unless can?(current_user, :read_commit_status, commit.project)
diff --git a/app/presenters/commit_status_presenter.rb b/app/presenters/commit_status_presenter.rb
index 3c39470b73..7919e501bf 100644
--- a/app/presenters/commit_status_presenter.rb
+++ b/app/presenters/commit_status_presenter.rb
@@ -28,19 +28,36 @@ class CommitStatusPresenter < Gitlab::View::Presenter::Delegated
ci_quota_exceeded: 'No more CI minutes available',
no_matching_runner: 'No matching runner available',
trace_size_exceeded: 'The job log size limit was reached',
- builds_disabled: 'The CI/CD is disabled for this project'
+ builds_disabled: 'The CI/CD is disabled for this project',
+ environment_creation_failure: 'This job could not be executed because it would create an environment with an invalid parameter.'
+ }.freeze
+
+ TROUBLESHOOTING_DOC = {
+ environment_creation_failure: { path: 'ci/environments/index', anchor: 'a-deployment-job-failed-with-this-job-could-not-be-executed-because-it-would-create-an-environment-with-an-invalid-parameter-error' }
}.freeze
private_constant :CALLOUT_FAILURE_MESSAGES
- presents :build
+ presents ::CommitStatus, as: :build
def self.callout_failure_messages
CALLOUT_FAILURE_MESSAGES
end
def callout_failure_message
- self.class.callout_failure_messages.fetch(failure_reason.to_sym)
+ message = self.class.callout_failure_messages.fetch(failure_reason.to_sym)
+
+ if doc = TROUBLESHOOTING_DOC[failure_reason.to_sym]
+ message += " #{help_page_link(doc[:path], doc[:anchor])}"
+ end
+
+ message
+ end
+
+ private
+
+ def help_page_link(path, anchor)
+ ActionController::Base.helpers.link_to('How do I fix it?', help_page_path(path, anchor: anchor))
end
end
diff --git a/app/presenters/dev_ops_report/metric_presenter.rb b/app/presenters/dev_ops_report/metric_presenter.rb
index 4d7ac1cd3e..55326f8f67 100644
--- a/app/presenters/dev_ops_report/metric_presenter.rb
+++ b/app/presenters/dev_ops_report/metric_presenter.rb
@@ -2,6 +2,8 @@
module DevOpsReport
class MetricPresenter < Gitlab::View::Presenter::Simple
+ presents ::DevOpsReport::Metric
+
delegate :created_at, to: :subject
def cards
diff --git a/app/presenters/environment_presenter.rb b/app/presenters/environment_presenter.rb
index 6f67bbe2a5..6c8da86187 100644
--- a/app/presenters/environment_presenter.rb
+++ b/app/presenters/environment_presenter.rb
@@ -3,7 +3,7 @@
class EnvironmentPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
- presents :environment
+ presents ::Environment, as: :environment
def path
project_environment_path(project, self)
diff --git a/app/presenters/event_presenter.rb b/app/presenters/event_presenter.rb
index c37721f721..4c787b88e2 100644
--- a/app/presenters/event_presenter.rb
+++ b/app/presenters/event_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class EventPresenter < Gitlab::View::Presenter::Delegated
- presents :event
+ presents ::Event, as: :event
def initialize(subject, **attributes)
super
@@ -10,6 +10,7 @@ class EventPresenter < Gitlab::View::Presenter::Delegated
end
# Caching `visible_to_user?` method in the presenter beause it might be called multiple times.
+ delegator_override :visible_to_user?
def visible_to_user?(user = nil)
@visible_to_user_cache.fetch(user&.id) { super(user) }
end
diff --git a/app/presenters/gitlab/blame_presenter.rb b/app/presenters/gitlab/blame_presenter.rb
index 1f2445b04a..e9340a42e5 100644
--- a/app/presenters/gitlab/blame_presenter.rb
+++ b/app/presenters/gitlab/blame_presenter.rb
@@ -12,7 +12,7 @@ module Gitlab
include TreeHelper
include IconsHelper
- presents :blame
+ presents nil, as: :blame
CommitData = Struct.new(
:author_avatar,
diff --git a/app/presenters/group_clusterable_presenter.rb b/app/presenters/group_clusterable_presenter.rb
index 34e7084ab0..c51cd41502 100644
--- a/app/presenters/group_clusterable_presenter.rb
+++ b/app/presenters/group_clusterable_presenter.rb
@@ -2,7 +2,8 @@
class GroupClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
- include ActionView::Helpers::UrlHelper
+
+ presents ::Group
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
@@ -31,7 +32,7 @@ class GroupClusterablePresenter < ClusterablePresenter
override :learn_more_link
def learn_more_link
- link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about group Kubernetes clusters'), help_page_path('user/group/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
def metrics_dashboard_path(cluster)
diff --git a/app/presenters/group_member_presenter.rb b/app/presenters/group_member_presenter.rb
index 5ab4b51f47..88facc3608 100644
--- a/app/presenters/group_member_presenter.rb
+++ b/app/presenters/group_member_presenter.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class GroupMemberPresenter < MemberPresenter
+ presents ::GroupMember
+
private
def admin_member_permission
diff --git a/app/presenters/instance_clusterable_presenter.rb b/app/presenters/instance_clusterable_presenter.rb
index 56d91f90b2..f2550eb17e 100644
--- a/app/presenters/instance_clusterable_presenter.rb
+++ b/app/presenters/instance_clusterable_presenter.rb
@@ -2,7 +2,8 @@
class InstanceClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
- include ActionView::Helpers::UrlHelper
+
+ presents ::Clusters::Instance
def self.fabricate(clusterable, **attributes)
attributes_with_presenter_class = attributes.merge(presenter_class: InstanceClusterablePresenter)
@@ -69,7 +70,7 @@ class InstanceClusterablePresenter < ClusterablePresenter
override :learn_more_link
def learn_more_link
- link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about instance Kubernetes clusters'), help_page_path('user/instance/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
def metrics_dashboard_path(cluster)
diff --git a/app/presenters/invitation_presenter.rb b/app/presenters/invitation_presenter.rb
index d8c07f327d..ada8227a47 100644
--- a/app/presenters/invitation_presenter.rb
+++ b/app/presenters/invitation_presenter.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class InvitationPresenter < Gitlab::View::Presenter::Delegated
- presents :invitation
+ presents nil, as: :invitation
end
diff --git a/app/presenters/issue_presenter.rb b/app/presenters/issue_presenter.rb
index b7f4ac0555..72fe14d224 100644
--- a/app/presenters/issue_presenter.rb
+++ b/app/presenters/issue_presenter.rb
@@ -1,12 +1,13 @@
# frozen_string_literal: true
class IssuePresenter < Gitlab::View::Presenter::Delegated
- presents :issue
+ presents ::Issue, as: :issue
def issue_path
url_builder.build(issue, only_path: true)
end
+ delegator_override :subscribed?
def subscribed?
issue.subscribed?(current_user, issue.project)
end
diff --git a/app/presenters/label_presenter.rb b/app/presenters/label_presenter.rb
index 9e51e6fa4b..fafade2828 100644
--- a/app/presenters/label_presenter.rb
+++ b/app/presenters/label_presenter.rb
@@ -1,9 +1,11 @@
# frozen_string_literal: true
class LabelPresenter < Gitlab::View::Presenter::Delegated
- presents :label
+ presents ::Label, as: :label
delegate :name, :full_name, to: :label_subject, prefix: :subject
+ delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `Label#subject`.
+
def edit_path
case label
when GroupLabel then edit_group_label_path(label.group, label)
diff --git a/app/presenters/member_presenter.rb b/app/presenters/member_presenter.rb
index b37a43bf25..67d044dd01 100644
--- a/app/presenters/member_presenter.rb
+++ b/app/presenters/member_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MemberPresenter < Gitlab::View::Presenter::Delegated
- presents :member
+ presents ::Member, as: :member
def access_level_roles
member.class.access_level_roles
diff --git a/app/presenters/members_presenter.rb b/app/presenters/members_presenter.rb
index 03ebea36d4..b572cf9623 100644
--- a/app/presenters/members_presenter.rb
+++ b/app/presenters/members_presenter.rb
@@ -3,7 +3,7 @@
class MembersPresenter < Gitlab::View::Presenter::Delegated
include Enumerable
- presents :members
+ presents nil, as: :members
def to_ary
to_a
diff --git a/app/presenters/merge_request_presenter.rb b/app/presenters/merge_request_presenter.rb
index fc8a290f5f..d19d496452 100644
--- a/app/presenters/merge_request_presenter.rb
+++ b/app/presenters/merge_request_presenter.rb
@@ -8,9 +8,11 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
include ChecksCollaboration
include Gitlab::Utils::StrongMemoize
+ delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
APPROVALS_WIDGET_BASE_TYPE = 'base'
- presents :merge_request
+ presents ::MergeRequest, as: :merge_request
def ci_status
if pipeline
@@ -183,6 +185,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
.can_push_to_branch?(source_branch)
end
+ delegator_override :can_remove_source_branch?
def can_remove_source_branch?
source_branch_exists? && merge_request.can_remove_source_branch?(current_user)
end
@@ -202,6 +205,7 @@ class MergeRequestPresenter < Gitlab::View::Presenter::Delegated
end
end
+ delegator_override :subscribed?
def subscribed?
merge_request.subscribed?(current_user, merge_request.target_project)
end
diff --git a/app/presenters/milestone_presenter.rb b/app/presenters/milestone_presenter.rb
index 6bf8203702..4084c8740f 100644
--- a/app/presenters/milestone_presenter.rb
+++ b/app/presenters/milestone_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class MilestonePresenter < Gitlab::View::Presenter::Delegated
- presents :milestone
+ presents ::Milestone, as: :milestone
def milestone_path
url_builder.build(milestone, only_path: true)
diff --git a/app/presenters/pages_domain_presenter.rb b/app/presenters/pages_domain_presenter.rb
index 6ef89760be..0523f70241 100644
--- a/app/presenters/pages_domain_presenter.rb
+++ b/app/presenters/pages_domain_presenter.rb
@@ -1,7 +1,9 @@
# frozen_string_literal: true
class PagesDomainPresenter < Gitlab::View::Presenter::Delegated
- presents :pages_domain
+ presents ::PagesDomain, as: :pages_domain
+
+ delegator_override :subject # TODO: Fix `Gitlab::View::Presenter::Delegated#subject` not to override `PagesDomain#subject`.
def needs_verification?
Gitlab::CurrentSettings.pages_domain_verification_enabled? && unverified?
diff --git a/app/presenters/project_clusterable_presenter.rb b/app/presenters/project_clusterable_presenter.rb
index 920304e743..6c4d1143c0 100644
--- a/app/presenters/project_clusterable_presenter.rb
+++ b/app/presenters/project_clusterable_presenter.rb
@@ -2,7 +2,8 @@
class ProjectClusterablePresenter < ClusterablePresenter
extend ::Gitlab::Utils::Override
- include ActionView::Helpers::UrlHelper
+
+ presents ::Project
override :cluster_status_cluster_path
def cluster_status_cluster_path(cluster, params = {})
@@ -26,7 +27,7 @@ class ProjectClusterablePresenter < ClusterablePresenter
override :learn_more_link
def learn_more_link
- link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
+ ApplicationController.helpers.link_to(s_('ClusterIntegration|Learn more about Kubernetes'), help_page_path('user/project/clusters/index'), target: '_blank', rel: 'noopener noreferrer')
end
def metrics_dashboard_path(cluster)
diff --git a/app/presenters/project_hook_presenter.rb b/app/presenters/project_hook_presenter.rb
index a65c7221b5..a696e9fd0e 100644
--- a/app/presenters/project_hook_presenter.rb
+++ b/app/presenters/project_hook_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ProjectHookPresenter < Gitlab::View::Presenter::Delegated
- presents :project_hook
+ presents ::ProjectHook, as: :project_hook
def logs_details_path(log)
project_hook_hook_log_path(project, self, log)
diff --git a/app/presenters/project_member_presenter.rb b/app/presenters/project_member_presenter.rb
index 17947266ed..91d3ae9687 100644
--- a/app/presenters/project_member_presenter.rb
+++ b/app/presenters/project_member_presenter.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class ProjectMemberPresenter < MemberPresenter
+ presents ::ProjectMember
+
private
def admin_member_permission
diff --git a/app/presenters/project_presenter.rb b/app/presenters/project_presenter.rb
index 066f4786cf..bbd8c715f5 100644
--- a/app/presenters/project_presenter.rb
+++ b/app/presenters/project_presenter.rb
@@ -12,7 +12,10 @@ class ProjectPresenter < Gitlab::View::Presenter::Delegated
include Gitlab::Utils::StrongMemoize
include Gitlab::Experiment::Dsl
- presents :project
+ delegator_override_with GitlabRoutingHelper # TODO: Remove `GitlabRoutingHelper` inclusion as it's duplicate
+ delegator_override_with Gitlab::Utils::StrongMemoize # TODO: Remove `Gitlab::Utils::StrongMemoize` inclusion as it's duplicate
+
+ presents ::Project, as: :project
AnchorData = Struct.new(:is_link, :label, :link, :class_modifier, :icon, :itemprop, :data)
MAX_TOPICS_TO_SHOW = 3
diff --git a/app/presenters/projects/import_export/project_export_presenter.rb b/app/presenters/projects/import_export/project_export_presenter.rb
index f56760b55d..7b2ffb6d75 100644
--- a/app/presenters/projects/import_export/project_export_presenter.rb
+++ b/app/presenters/projects/import_export/project_export_presenter.rb
@@ -5,16 +5,24 @@ module Projects
class ProjectExportPresenter < Gitlab::View::Presenter::Delegated
include ActiveModel::Serializers::JSON
- presents :project
+ presents ::Project, as: :project
+ # TODO: Remove `ActiveModel::Serializers::JSON` inclusion as it's duplicate
+ delegator_override_with ActiveModel::Serializers::JSON
+ delegator_override_with ActiveModel::Naming
+ delegator_override :include_root_in_json, :include_root_in_json?
+
+ delegator_override :project_members
def project_members
super + converted_group_members
end
+ delegator_override :description
def description
self.respond_to?(:override_description) ? override_description : super
end
+ delegator_override :protected_branches
def protected_branches
project.exported_protected_branches
end
diff --git a/app/presenters/projects/settings/deploy_keys_presenter.rb b/app/presenters/projects/settings/deploy_keys_presenter.rb
index 13290a8e63..e3323b7518 100644
--- a/app/presenters/projects/settings/deploy_keys_presenter.rb
+++ b/app/presenters/projects/settings/deploy_keys_presenter.rb
@@ -5,7 +5,7 @@ module Projects
class DeployKeysPresenter < Gitlab::View::Presenter::Simple
include Gitlab::Utils::StrongMemoize
- presents :project
+ presents ::Project, as: :project
delegate :size, to: :enabled_keys, prefix: true
delegate :size, to: :available_project_keys, prefix: true
delegate :size, to: :available_public_keys, prefix: true
diff --git a/app/presenters/prometheus_alert_presenter.rb b/app/presenters/prometheus_alert_presenter.rb
index 99e24bdcdb..714329ede7 100644
--- a/app/presenters/prometheus_alert_presenter.rb
+++ b/app/presenters/prometheus_alert_presenter.rb
@@ -3,7 +3,7 @@
class PrometheusAlertPresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
- presents :prometheus_alert
+ presents ::PrometheusAlert, as: :prometheus_alert
def humanized_text
operator_text =
diff --git a/app/presenters/release_presenter.rb b/app/presenters/release_presenter.rb
index ac27e997b4..c919c7f4c6 100644
--- a/app/presenters/release_presenter.rb
+++ b/app/presenters/release_presenter.rb
@@ -3,8 +3,10 @@
class ReleasePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
- presents :release
+ presents ::Release, as: :release
+ # TODO: Remove `delegate` as it's redundant due to SimpleDelegator.
+ delegator_override :tag, :project
delegate :project, :tag, to: :release
def commit_path
@@ -51,6 +53,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
edit_project_release_url(project, release)
end
+ delegator_override :assets_count
def assets_count
if can_download_code?
release.assets_count
@@ -59,6 +62,7 @@ class ReleasePresenter < Gitlab::View::Presenter::Delegated
end
end
+ delegator_override :name
def name
can_download_code? ? release.name : "Release-#{release.id}"
end
diff --git a/app/presenters/releases/evidence_presenter.rb b/app/presenters/releases/evidence_presenter.rb
index a00cbacb7d..bdc053a303 100644
--- a/app/presenters/releases/evidence_presenter.rb
+++ b/app/presenters/releases/evidence_presenter.rb
@@ -4,7 +4,7 @@ module Releases
class EvidencePresenter < Gitlab::View::Presenter::Delegated
include ActionView::Helpers::UrlHelper
- presents :evidence
+ presents ::Releases::Evidence, as: :evidence
def filepath
release = evidence.release
diff --git a/app/presenters/search_service_presenter.rb b/app/presenters/search_service_presenter.rb
index ab43800b9f..72f967b8be 100644
--- a/app/presenters/search_service_presenter.rb
+++ b/app/presenters/search_service_presenter.rb
@@ -3,7 +3,7 @@
class SearchServicePresenter < Gitlab::View::Presenter::Delegated
include RendersCommits
- presents :search_service
+ presents ::SearchService, as: :search_service
SCOPE_PRELOAD_METHOD = {
projects: :with_web_entity_associations,
@@ -18,6 +18,7 @@ class SearchServicePresenter < Gitlab::View::Presenter::Delegated
SORT_ENABLED_SCOPES = %w(issues merge_requests epics).freeze
+ delegator_override :search_objects
def search_objects
@search_objects ||= begin
objects = search_service.search_objects(SCOPE_PRELOAD_METHOD[scope.to_sym])
diff --git a/app/presenters/sentry_error_presenter.rb b/app/presenters/sentry_error_presenter.rb
index 669bcb68b7..5862e54dfc 100644
--- a/app/presenters/sentry_error_presenter.rb
+++ b/app/presenters/sentry_error_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class SentryErrorPresenter < Gitlab::View::Presenter::Delegated
- presents :error
+ presents nil, as: :error
FrequencyStruct = Struct.new(:time, :count, keyword_init: true)
diff --git a/app/presenters/service_hook_presenter.rb b/app/presenters/service_hook_presenter.rb
index 8f2ba1a905..91911eb3df 100644
--- a/app/presenters/service_hook_presenter.rb
+++ b/app/presenters/service_hook_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class ServiceHookPresenter < Gitlab::View::Presenter::Delegated
- presents :service_hook
+ presents ::ServiceHook, as: :service_hook
def logs_details_path(log)
project_service_hook_log_path(integration.project, integration, log)
diff --git a/app/presenters/snippet_blob_presenter.rb b/app/presenters/snippet_blob_presenter.rb
index ab8fc0f905..4072696eb8 100644
--- a/app/presenters/snippet_blob_presenter.rb
+++ b/app/presenters/snippet_blob_presenter.rb
@@ -3,6 +3,8 @@
class SnippetBlobPresenter < BlobPresenter
include GitlabRoutingHelper
+ presents ::SnippetBlob
+
def rich_data
return unless blob.rich_viewer
diff --git a/app/presenters/snippet_presenter.rb b/app/presenters/snippet_presenter.rb
index 695aa266e2..8badbe7f54 100644
--- a/app/presenters/snippet_presenter.rb
+++ b/app/presenters/snippet_presenter.rb
@@ -1,16 +1,18 @@
# frozen_string_literal: true
class SnippetPresenter < Gitlab::View::Presenter::Delegated
- presents :snippet
+ presents ::Snippet, as: :snippet
def raw_url
url_builder.build(snippet, raw: true)
end
+ delegator_override :ssh_url_to_repo
def ssh_url_to_repo
snippet.ssh_url_to_repo if snippet.repository_exists?
end
+ delegator_override :http_url_to_repo
def http_url_to_repo
snippet.http_url_to_repo if snippet.repository_exists?
end
@@ -31,6 +33,7 @@ class SnippetPresenter < Gitlab::View::Presenter::Delegated
snippet.submittable_as_spam_by?(current_user)
end
+ delegator_override :blob
def blob
return snippet.blob if snippet.empty_repo?
diff --git a/app/presenters/terraform/modules_presenter.rb b/app/presenters/terraform/modules_presenter.rb
index 608f69e201..9e9c6a5cd2 100644
--- a/app/presenters/terraform/modules_presenter.rb
+++ b/app/presenters/terraform/modules_presenter.rb
@@ -4,7 +4,7 @@ module Terraform
class ModulesPresenter < Gitlab::View::Presenter::Simple
attr_accessor :packages, :system
- presents :modules
+ presents nil, as: :modules
def initialize(packages, system)
@packages = packages
diff --git a/app/presenters/todo_presenter.rb b/app/presenters/todo_presenter.rb
index 291be7848e..cb8d37be22 100644
--- a/app/presenters/todo_presenter.rb
+++ b/app/presenters/todo_presenter.rb
@@ -1,5 +1,5 @@
# frozen_string_literal: true
class TodoPresenter < Gitlab::View::Presenter::Delegated
- presents :todo
+ presents ::Todo, as: :todo
end
diff --git a/app/presenters/tree_entry_presenter.rb b/app/presenters/tree_entry_presenter.rb
index 216b3b0d4c..0b313d8136 100644
--- a/app/presenters/tree_entry_presenter.rb
+++ b/app/presenters/tree_entry_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class TreeEntryPresenter < Gitlab::View::Presenter::Delegated
- presents :tree
+ presents nil, as: :tree
def web_url
Gitlab::Routing.url_helpers.project_tree_url(tree.repository.project, File.join(tree.commit_id, tree.path))
diff --git a/app/presenters/user_presenter.rb b/app/presenters/user_presenter.rb
index 7cd94082ba..5a99f10b6e 100644
--- a/app/presenters/user_presenter.rb
+++ b/app/presenters/user_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class UserPresenter < Gitlab::View::Presenter::Delegated
- presents :user
+ presents ::User, as: :user
def group_memberships
should_be_private? ? GroupMember.none : user.group_members
diff --git a/app/presenters/web_hook_log_presenter.rb b/app/presenters/web_hook_log_presenter.rb
index fca03ddb5d..a516658907 100644
--- a/app/presenters/web_hook_log_presenter.rb
+++ b/app/presenters/web_hook_log_presenter.rb
@@ -1,7 +1,7 @@
# frozen_string_literal: true
class WebHookLogPresenter < Gitlab::View::Presenter::Delegated
- presents :web_hook_log
+ presents ::WebHookLog, as: :web_hook_log
def details_path
web_hook.present.logs_details_path(self)
diff --git a/app/serializers/feature_flag_entity.rb b/app/serializers/feature_flag_entity.rb
index 80cf869a38..196a4cd504 100644
--- a/app/serializers/feature_flag_entity.rb
+++ b/app/serializers/feature_flag_entity.rb
@@ -24,8 +24,8 @@ class FeatureFlagEntity < Grape::Entity
project_feature_flag_path(feature_flag.project, feature_flag)
end
- expose :scopes, with: FeatureFlagScopeEntity do |feature_flag|
- feature_flag.scopes.sort_by(&:id)
+ expose :scopes do |_ff|
+ []
end
expose :strategies, with: FeatureFlags::StrategyEntity do |feature_flag|
diff --git a/app/serializers/feature_flag_scope_entity.rb b/app/serializers/feature_flag_scope_entity.rb
deleted file mode 100644
index 0450797a54..0000000000
--- a/app/serializers/feature_flag_scope_entity.rb
+++ /dev/null
@@ -1,12 +0,0 @@
-# frozen_string_literal: true
-
-class FeatureFlagScopeEntity < Grape::Entity
- include RequestAwareEntity
-
- expose :id
- expose :active
- expose :environment_scope
- expose :created_at
- expose :updated_at
- expose :strategies
-end
diff --git a/app/serializers/member_entity.rb b/app/serializers/member_entity.rb
index 5100a41638..d7221109ec 100644
--- a/app/serializers/member_entity.rb
+++ b/app/serializers/member_entity.rb
@@ -44,6 +44,8 @@ class MemberEntity < Grape::Entity
MemberUserEntity.represent(member.user, source: options[:source])
end
+ expose :state
+
expose :invite, if: -> (member) { member.invite? } do
expose :email do |member|
member.invite_email
@@ -56,6 +58,10 @@ class MemberEntity < Grape::Entity
expose :can_resend do |member|
member.can_resend_invite?
end
+
+ expose :user_state do |member|
+ member.respond_to?(:invited_user_state) ? member.invited_user_state : ""
+ end
end
end
diff --git a/app/serializers/merge_request_metrics_helper.rb b/app/serializers/merge_request_metrics_helper.rb
new file mode 100644
index 0000000000..fb1769d0aa
--- /dev/null
+++ b/app/serializers/merge_request_metrics_helper.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module MergeRequestMetricsHelper
+ # There are cases where where metrics object doesn't exist and it needs to be rebuilt.
+ # TODO: Once https://gitlab.com/gitlab-org/gitlab/-/issues/342508 has been resolved and
+ # all merge requests have metrics we can remove this helper method.
+ def build_metrics(merge_request)
+ # There's no need to query and serialize metrics data for merge requests that are not
+ # merged or closed.
+ return unless merge_request.merged? || merge_request.closed?
+ return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
+ return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
+
+ build_metrics_from_events(merge_request)
+ end
+
+ private
+
+ def build_metrics_from_events(merge_request)
+ closed_event = merge_request.closed_event
+ merge_event = merge_request.merge_event
+
+ MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
+ latest_closed_by: closed_event&.author,
+ merged_at: merge_event&.updated_at,
+ merged_by: merge_event&.author)
+ end
+end
diff --git a/app/serializers/merge_request_poll_cached_widget_entity.rb b/app/serializers/merge_request_poll_cached_widget_entity.rb
index 7fba52cbe1..8b0f3c8eb7 100644
--- a/app/serializers/merge_request_poll_cached_widget_entity.rb
+++ b/app/serializers/merge_request_poll_cached_widget_entity.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
class MergeRequestPollCachedWidgetEntity < IssuableEntity
+ include MergeRequestMetricsHelper
+
expose :auto_merge_enabled
expose :state
expose :merged_commit_sha
@@ -158,29 +160,6 @@ class MergeRequestPollCachedWidgetEntity < IssuableEntity
@presenters ||= {}
@presenters[merge_request] ||= MergeRequestPresenter.new(merge_request, current_user: current_user) # rubocop: disable CodeReuse/Presenter
end
-
- # Once SchedulePopulateMergeRequestMetricsWithEventsData fully runs,
- # we can remove this method and just serialize MergeRequest#metrics
- # instead. See https://gitlab.com/gitlab-org/gitlab-foss/issues/41587
- def build_metrics(merge_request)
- # There's no need to query and serialize metrics data for merge requests that are not
- # merged or closed.
- return unless merge_request.merged? || merge_request.closed?
- return merge_request.metrics if merge_request.merged? && merge_request.metrics&.merged_by_id
- return merge_request.metrics if merge_request.closed? && merge_request.metrics&.latest_closed_by_id
-
- build_metrics_from_events(merge_request)
- end
-
- def build_metrics_from_events(merge_request)
- closed_event = merge_request.closed_event
- merge_event = merge_request.merge_event
-
- MergeRequest::Metrics.new(latest_closed_at: closed_event&.updated_at,
- latest_closed_by: closed_event&.author,
- merged_at: merge_event&.updated_at,
- merged_by: merge_event&.author)
- end
end
MergeRequestPollCachedWidgetEntity.prepend_mod_with('MergeRequestPollCachedWidgetEntity')
diff --git a/app/serializers/merge_request_widget_entity.rb b/app/serializers/merge_request_widget_entity.rb
index 1c033dee5f..1e4289ce77 100644
--- a/app/serializers/merge_request_widget_entity.rb
+++ b/app/serializers/merge_request_widget_entity.rb
@@ -83,7 +83,10 @@ class MergeRequestWidgetEntity < Grape::Entity
end
expose :is_dismissed_suggest_pipeline do |_merge_request|
- current_user && current_user.dismissed_callout?(feature_name: SUGGEST_PIPELINE)
+ next true unless current_user
+ next true unless Gitlab::CurrentSettings.suggest_pipeline_enabled?
+
+ current_user.dismissed_callout?(feature_name: SUGGEST_PIPELINE)
end
expose :human_access do |merge_request|
diff --git a/app/services/base_project_service.rb b/app/services/base_project_service.rb
index fb466e6167..1bf4a235a7 100644
--- a/app/services/base_project_service.rb
+++ b/app/services/base_project_service.rb
@@ -2,6 +2,8 @@
# Base class, scoped by project
class BaseProjectService < ::BaseContainerService
+ include ::Gitlab::Utils::StrongMemoize
+
attr_accessor :project
def initialize(project:, current_user: nil, params: {})
@@ -11,4 +13,12 @@ class BaseProjectService < ::BaseContainerService
end
delegate :repository, to: :project
+
+ private
+
+ def project_group
+ strong_memoize(:project_group) do
+ project.group
+ end
+ end
end
diff --git a/app/services/boards/issues/list_service.rb b/app/services/boards/issues/list_service.rb
index 9a3e3bc3bd..6021d634f8 100644
--- a/app/services/boards/issues/list_service.rb
+++ b/app/services/boards/issues/list_service.rb
@@ -22,7 +22,7 @@ module Boards
def order(items)
return items.order_closed_date_desc if list&.closed?
- items.order_by_position_and_priority(with_cte: params[:search].present?)
+ items.order_by_relative_position
end
def finder
diff --git a/app/services/bulk_import_service.rb b/app/services/bulk_import_service.rb
deleted file mode 100644
index 4e13e967db..0000000000
--- a/app/services/bulk_import_service.rb
+++ /dev/null
@@ -1,70 +0,0 @@
-# frozen_string_literal: true
-
-# Entry point of the BulkImport feature.
-# This service receives a Gitlab Instance connection params
-# and a list of groups to be imported.
-#
-# Process topography:
-#
-# sync | async
-# |
-# User +--> P1 +----> Pn +---+
-# | ^ | Enqueue new job
-# | +-----+
-#
-# P1 (sync)
-#
-# - Create a BulkImport record
-# - Create a BulkImport::Entity for each group to be imported
-# - Enqueue a BulkImportWorker job (P2) to import the given groups (entities)
-#
-# Pn (async)
-#
-# - For each group to be imported (BulkImport::Entity.with_status(:created))
-# - Import the group data
-# - Create entities for each subgroup of the imported group
-# - Enqueue a BulkImportService job (Pn) to import the new entities (subgroups)
-#
-class BulkImportService
- attr_reader :current_user, :params, :credentials
-
- def initialize(current_user, params, credentials)
- @current_user = current_user
- @params = params
- @credentials = credentials
- end
-
- def execute
- bulk_import = create_bulk_import
-
- BulkImportWorker.perform_async(bulk_import.id)
-
- ServiceResponse.success(payload: bulk_import)
- rescue ActiveRecord::RecordInvalid => e
- ServiceResponse.error(
- message: e.message,
- http_status: :unprocessable_entity
- )
- end
-
- private
-
- def create_bulk_import
- BulkImport.transaction do
- bulk_import = BulkImport.create!(user: current_user, source_type: 'gitlab')
- bulk_import.create_configuration!(credentials.slice(:url, :access_token))
-
- params.each do |entity|
- BulkImports::Entity.create!(
- bulk_import: bulk_import,
- source_type: entity[:source_type],
- source_full_path: entity[:source_full_path],
- destination_name: entity[:destination_name],
- destination_namespace: entity[:destination_namespace]
- )
- end
-
- bulk_import
- end
- end
-end
diff --git a/app/services/bulk_imports/create_service.rb b/app/services/bulk_imports/create_service.rb
new file mode 100644
index 0000000000..c1becbb560
--- /dev/null
+++ b/app/services/bulk_imports/create_service.rb
@@ -0,0 +1,83 @@
+# frozen_string_literal: true
+
+# Entry point of the BulkImport feature.
+# This service receives a Gitlab Instance connection params
+# and a list of groups to be imported.
+#
+# Process topography:
+#
+# sync | async
+# |
+# User +--> P1 +----> Pn +---+
+# | ^ | Enqueue new job
+# | +-----+
+#
+# P1 (sync)
+#
+# - Create a BulkImport record
+# - Create a BulkImport::Entity for each group to be imported
+# - Enqueue a BulkImportWorker job (P2) to import the given groups (entities)
+#
+# Pn (async)
+#
+# - For each group to be imported (BulkImport::Entity.with_status(:created))
+# - Import the group data
+# - Create entities for each subgroup of the imported group
+# - Enqueue a BulkImports::CreateService job (Pn) to import the new entities (subgroups)
+#
+module BulkImports
+ class CreateService
+ attr_reader :current_user, :params, :credentials
+
+ def initialize(current_user, params, credentials)
+ @current_user = current_user
+ @params = params
+ @credentials = credentials
+ end
+
+ def execute
+ bulk_import = create_bulk_import
+
+ BulkImportWorker.perform_async(bulk_import.id)
+
+ ServiceResponse.success(payload: bulk_import)
+ rescue ActiveRecord::RecordInvalid => e
+ ServiceResponse.error(
+ message: e.message,
+ http_status: :unprocessable_entity
+ )
+ end
+
+ private
+
+ def create_bulk_import
+ BulkImport.transaction do
+ bulk_import = BulkImport.create!(
+ user: current_user,
+ source_type: 'gitlab',
+ source_version: client.instance_version
+ )
+ bulk_import.create_configuration!(credentials.slice(:url, :access_token))
+
+ params.each do |entity|
+ BulkImports::Entity.create!(
+ bulk_import: bulk_import,
+ source_type: entity[:source_type],
+ source_full_path: entity[:source_full_path],
+ destination_name: entity[:destination_name],
+ destination_namespace: entity[:destination_namespace]
+ )
+ end
+
+ bulk_import
+ end
+ end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token]
+ )
+ end
+ end
+end
diff --git a/app/services/bulk_imports/file_export_service.rb b/app/services/bulk_imports/file_export_service.rb
new file mode 100644
index 0000000000..a7e0f99866
--- /dev/null
+++ b/app/services/bulk_imports/file_export_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class FileExportService
+ include Gitlab::ImportExport::CommandLineUtil
+
+ def initialize(portable, export_path, relation)
+ @portable = portable
+ @export_path = export_path
+ @relation = relation
+ end
+
+ def execute
+ export_service.execute
+
+ archive_exported_data
+ end
+
+ def exported_filename
+ "#{relation}.tar"
+ end
+
+ private
+
+ attr_reader :export_path, :portable, :relation
+
+ def export_service
+ case relation
+ when FileTransfer::ProjectConfig::UPLOADS_RELATION
+ UploadsExportService.new(portable, export_path)
+ else
+ raise BulkImports::Error, 'Unsupported relation export type'
+ end
+ end
+
+ def archive_exported_data
+ archive_file = File.join(export_path, exported_filename)
+
+ tar_cf(archive: archive_file, dir: export_path)
+ end
+ end
+end
diff --git a/app/services/bulk_imports/get_importable_data_service.rb b/app/services/bulk_imports/get_importable_data_service.rb
new file mode 100644
index 0000000000..07e0b3976a
--- /dev/null
+++ b/app/services/bulk_imports/get_importable_data_service.rb
@@ -0,0 +1,45 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class GetImportableDataService
+ def initialize(params, query_params, credentials)
+ @params = params
+ @query_params = query_params
+ @credentials = credentials
+ end
+
+ def execute
+ {
+ version_validation: version_validation,
+ response: importables
+ }
+ end
+
+ private
+
+ def importables
+ client.get('groups', @query_params)
+ end
+
+ def version_validation
+ {
+ features: {
+ project_migration: {
+ available: client.compatible_for_project_migration?,
+ min_version: BulkImport.min_gl_version_for_project_migration.to_s
+ },
+ source_instance_version: client.instance_version.to_s
+ }
+ }
+ end
+
+ def client
+ @client ||= BulkImports::Clients::HTTP.new(
+ url: @credentials[:url],
+ token: @credentials[:access_token],
+ per_page: @params[:per_page],
+ page: @params[:page]
+ )
+ end
+ end
+end
diff --git a/app/services/bulk_imports/relation_export_service.rb b/app/services/bulk_imports/relation_export_service.rb
index 055f9cafd1..4718b3914b 100644
--- a/app/services/bulk_imports/relation_export_service.rb
+++ b/app/services/bulk_imports/relation_export_service.rb
@@ -9,20 +9,23 @@ module BulkImports
@portable = portable
@relation = relation
@jid = jid
+ @config = FileTransfer.config_for(portable)
end
def execute
find_or_create_export! do |export|
remove_existing_export_file!(export)
- serialize_relation_to_file(export.relation_definition)
+ export_service.execute
compress_exported_relation
upload_compressed_file(export)
end
+ ensure
+ FileUtils.remove_entry(config.export_path)
end
private
- attr_reader :user, :portable, :relation, :jid
+ attr_reader :user, :portable, :relation, :jid, :config
def find_or_create_export!
validate_user_permissions!
@@ -55,52 +58,28 @@ module BulkImports
upload.save!
end
- def serialize_relation_to_file(relation_definition)
- serializer.serialize_relation(relation_definition)
- end
-
- def compress_exported_relation
- gzip(dir: export_path, filename: ndjson_filename)
+ def export_service
+ @export_service ||= if config.tree_relation?(relation)
+ TreeExportService.new(portable, config.export_path, relation)
+ elsif config.file_relation?(relation)
+ FileExportService.new(portable, config.export_path, relation)
+ else
+ raise BulkImports::Error, 'Unsupported export relation'
+ end
end
def upload_compressed_file(export)
- compressed_filename = File.join(export_path, "#{ndjson_filename}.gz")
+ compressed_file = File.join(config.export_path, "#{export_service.exported_filename}.gz")
+
upload = ExportUpload.find_or_initialize_by(export_id: export.id) # rubocop: disable CodeReuse/ActiveRecord
- File.open(compressed_filename) { |file| upload.export_file = file }
+ File.open(compressed_file) { |file| upload.export_file = file }
upload.save!
end
- def config
- @config ||= FileTransfer.config_for(portable)
- end
-
- def export_path
- @export_path ||= config.export_path
- end
-
- def portable_tree
- @portable_tree ||= config.portable_tree
- end
-
- # rubocop: disable CodeReuse/Serializer
- def serializer
- @serializer ||= ::Gitlab::ImportExport::Json::StreamingSerializer.new(
- portable,
- portable_tree,
- json_writer,
- exportable_path: ''
- )
- end
- # rubocop: enable CodeReuse/Serializer
-
- def json_writer
- @json_writer ||= ::Gitlab::ImportExport::Json::NdjsonWriter.new(export_path)
- end
-
- def ndjson_filename
- @ndjson_filename ||= "#{relation}.ndjson"
+ def compress_exported_relation
+ gzip(dir: config.export_path, filename: export_service.exported_filename)
end
end
end
diff --git a/app/services/bulk_imports/tree_export_service.rb b/app/services/bulk_imports/tree_export_service.rb
new file mode 100644
index 0000000000..b8e7ac4574
--- /dev/null
+++ b/app/services/bulk_imports/tree_export_service.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class TreeExportService
+ def initialize(portable, export_path, relation)
+ @portable = portable
+ @export_path = export_path
+ @relation = relation
+ @config = FileTransfer.config_for(portable)
+ end
+
+ def execute
+ relation_definition = config.tree_relation_definition_for(relation)
+
+ raise BulkImports::Error, 'Unsupported relation export type' unless relation_definition
+
+ serializer.serialize_relation(relation_definition)
+ end
+
+ def exported_filename
+ "#{relation}.ndjson"
+ end
+
+ private
+
+ attr_reader :export_path, :portable, :relation, :config
+
+ # rubocop: disable CodeReuse/Serializer
+ def serializer
+ ::Gitlab::ImportExport::Json::StreamingSerializer.new(
+ portable,
+ config.portable_tree,
+ json_writer,
+ exportable_path: ''
+ )
+ end
+ # rubocop: enable CodeReuse/Serializer
+
+ def json_writer
+ ::Gitlab::ImportExport::Json::NdjsonWriter.new(export_path)
+ end
+ end
+end
diff --git a/app/services/bulk_imports/uploads_export_service.rb b/app/services/bulk_imports/uploads_export_service.rb
new file mode 100644
index 0000000000..32cc48c152
--- /dev/null
+++ b/app/services/bulk_imports/uploads_export_service.rb
@@ -0,0 +1,49 @@
+# frozen_string_literal: true
+
+module BulkImports
+ class UploadsExportService
+ include Gitlab::ImportExport::CommandLineUtil
+
+ BATCH_SIZE = 100
+
+ def initialize(portable, export_path)
+ @portable = portable
+ @export_path = export_path
+ end
+
+ def execute
+ portable.uploads.find_each(batch_size: BATCH_SIZE) do |upload| # rubocop: disable CodeReuse/ActiveRecord
+ uploader = upload.retrieve_uploader
+
+ next unless upload.exist?
+ next unless uploader.file
+
+ subdir_path = export_subdir_path(upload)
+ mkdir_p(subdir_path)
+ download_or_copy_upload(uploader, File.join(subdir_path, uploader.filename))
+ rescue Errno::ENAMETOOLONG => e
+ # Do not fail entire export process if downloaded file has filename that exceeds 255 characters.
+ # Ignore raised exception, skip such upload, log the error and keep going with the export instead.
+ Gitlab::ErrorTracking.log_exception(e, portable_id: portable.id, portable_class: portable.class.name, upload_id: upload.id)
+ end
+ end
+
+ private
+
+ attr_reader :portable, :export_path
+
+ def export_subdir_path(upload)
+ subdir = if upload.path == avatar_path
+ 'avatar'
+ else
+ upload.try(:secret).to_s
+ end
+
+ File.join(export_path, subdir)
+ end
+
+ def avatar_path
+ @avatar_path ||= portable.avatar&.upload&.path
+ end
+ end
+end
diff --git a/app/services/ci/archive_trace_service.rb b/app/services/ci/archive_trace_service.rb
index 995b58c688..17cac38ace 100644
--- a/app/services/ci/archive_trace_service.rb
+++ b/app/services/ci/archive_trace_service.rb
@@ -3,10 +3,15 @@
module Ci
class ArchiveTraceService
def execute(job, worker_name:)
+ unless job.trace.archival_attempts_available?
+ Sidekiq.logger.warn(class: worker_name, message: 'The job is out of archival attempts.', job_id: job.id)
+
+ job.trace.attempt_archive_cleanup!
+ return
+ end
+
unless job.trace.can_attempt_archival_now?
- Sidekiq.logger.warn(class: worker_name,
- message: job.trace.archival_attempts_message,
- job_id: job.id)
+ Sidekiq.logger.warn(class: worker_name, message: 'The job can not be archived right now.', job_id: job.id)
return
end
diff --git a/app/services/ci/destroy_pipeline_service.rb b/app/services/ci/destroy_pipeline_service.rb
index dd5c8e0379..476c7523d6 100644
--- a/app/services/ci/destroy_pipeline_service.rb
+++ b/app/services/ci/destroy_pipeline_service.rb
@@ -9,6 +9,9 @@ module Ci
pipeline.cancel_running if pipeline.cancelable?
+ # Ci::Pipeline#destroy triggers `use_fast_destroy :job_artifacts` and
+ # ci_builds has ON DELETE CASCADE to ci_pipelines. The pipeline, the builds,
+ # job and pipeline artifacts all get destroyed here.
pipeline.reset.destroy!
ServiceResponse.success(message: 'Pipeline not found')
diff --git a/app/services/ci/pipelines/add_job_service.rb b/app/services/ci/pipelines/add_job_service.rb
index 53536b6fdf..703bb22fb5 100644
--- a/app/services/ci/pipelines/add_job_service.rb
+++ b/app/services/ci/pipelines/add_job_service.rb
@@ -16,15 +16,7 @@ module Ci
def execute!(job, &block)
assign_pipeline_attributes(job)
- if Feature.enabled?(:ci_pipeline_add_job_with_lock, pipeline.project, default_enabled: :yaml)
- in_lock("ci:pipelines:#{pipeline.id}:add-job", ttl: LOCK_TIMEOUT, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES) do
- Ci::Pipeline.transaction do
- yield(job)
-
- job.update_older_statuses_retried!
- end
- end
- else
+ in_lock("ci:pipelines:#{pipeline.id}:add-job", ttl: LOCK_TIMEOUT, sleep_sec: LOCK_SLEEP, retries: LOCK_RETRIES) do
Ci::Pipeline.transaction do
yield(job)
diff --git a/app/services/ci/pipelines/hook_service.rb b/app/services/ci/pipelines/hook_service.rb
new file mode 100644
index 0000000000..629ed7e1eb
--- /dev/null
+++ b/app/services/ci/pipelines/hook_service.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Ci
+ module Pipelines
+ class HookService
+ include Gitlab::Utils::StrongMemoize
+
+ HOOK_NAME = :pipeline_hooks
+
+ def initialize(pipeline)
+ @pipeline = pipeline
+ end
+
+ def execute
+ project.execute_hooks(hook_data, HOOK_NAME) if project.has_active_hooks?(HOOK_NAME)
+ project.execute_integrations(hook_data, HOOK_NAME) if project.has_active_integrations?(HOOK_NAME)
+ end
+
+ private
+
+ attr_reader :pipeline
+
+ def project
+ @project ||= pipeline.project
+ end
+
+ def hook_data
+ strong_memoize(:hook_data) do
+ Gitlab::DataBuilder::Pipeline.build(pipeline)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index fb26d5d335..664915c5e2 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -11,8 +11,6 @@ module Ci
def execute
increment_processing_counter
- update_retried
-
Ci::PipelineProcessing::AtomicProcessingService
.new(pipeline)
.execute
@@ -24,41 +22,6 @@ module Ci
private
- # This method is for compatibility and data consistency and should be removed with 9.3 version of GitLab
- # This replicates what is db/post_migrate/20170416103934_upate_retried_for_ci_build.rb
- # and ensures that functionality will not be broken before migration is run
- # this updates only when there are data that needs to be updated, there are two groups with no retried flag
- # rubocop: disable CodeReuse/ActiveRecord
- def update_retried
- return if Feature.enabled?(:ci_remove_update_retried_from_process_pipeline, pipeline.project, default_enabled: :yaml)
-
- # find the latest builds for each name
- latest_statuses = pipeline.latest_statuses
- .group(:name)
- .having('count(*) > 1')
- .pluck(Arel.sql('MAX(id)'), 'name')
-
- # mark builds that are retried
- if latest_statuses.any?
- updated_count = pipeline.latest_statuses
- .where(name: latest_statuses.map(&:second))
- .where.not(id: latest_statuses.map(&:first))
- .update_all(retried: true)
-
- # This counter is temporary. It will be used to check whether if we still use this method or not
- # after setting correct value of `GenericCommitStatus#retried`.
- # More info: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/50465#note_491657115
- if updated_count > 0
- Gitlab::AppJsonLogger.info(event: 'update_retried_is_used',
- project_id: pipeline.project.id,
- pipeline_id: pipeline.id)
-
- metrics.legacy_update_jobs_counter.increment
- end
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
def increment_processing_counter
metrics.pipeline_processing_events_counter.increment
end
diff --git a/app/services/ci/queue/build_queue_service.rb b/app/services/ci/queue/build_queue_service.rb
index 3276c42792..3c886cb023 100644
--- a/app/services/ci/queue/build_queue_service.rb
+++ b/app/services/ci/queue/build_queue_service.rb
@@ -90,7 +90,7 @@ module Ci
def runner_projects_relation
if ::Feature.enabled?(:ci_pending_builds_project_runners_decoupling, runner, default_enabled: :yaml)
- runner.runner_projects.select(:project_id)
+ runner.runner_projects.select('"ci_runner_projects"."project_id"::bigint')
else
runner.projects.without_deleted.with_builds_enabled
end
diff --git a/app/services/ci/register_job_service.rb b/app/services/ci/register_job_service.rb
index c46ddd2255..67ef4f1070 100644
--- a/app/services/ci/register_job_service.rb
+++ b/app/services/ci/register_job_service.rb
@@ -22,7 +22,8 @@ module Ci
end
def execute(params = {})
- db_all_caught_up = ::Gitlab::Database::LoadBalancing::Sticking.all_caught_up?(:runner, runner.id)
+ db_all_caught_up =
+ ::Ci::Runner.sticking.all_caught_up?(:runner, runner.id)
@metrics.increment_queue_operation(:queue_attempt)
@@ -103,42 +104,40 @@ module Ci
# rubocop: disable CodeReuse/ActiveRecord
def each_build(params, &blk)
- ::Gitlab::Database.allow_cross_joins_across_databases(url: 'https://gitlab.com/gitlab-org/gitlab/-/issues/339429') do
- queue = ::Ci::Queue::BuildQueueService.new(runner)
+ queue = ::Ci::Queue::BuildQueueService.new(runner)
- builds = begin
- if runner.instance_type?
- queue.builds_for_shared_runner
- elsif runner.group_type?
- queue.builds_for_group_runner
- else
- queue.builds_for_project_runner
- end
+ builds = begin
+ if runner.instance_type?
+ queue.builds_for_shared_runner
+ elsif runner.group_type?
+ queue.builds_for_group_runner
+ else
+ queue.builds_for_project_runner
end
-
- if runner.ref_protected?
- builds = queue.builds_for_protected_runner(builds)
- end
-
- # pick builds that does not have other tags than runner's one
- builds = queue.builds_matching_tag_ids(builds, runner.tags.ids)
-
- # pick builds that have at least one tag
- unless runner.run_untagged?
- builds = queue.builds_with_any_tags(builds)
- end
-
- # pick builds that older than specified age
- if params.key?(:job_age)
- builds = queue.builds_queued_before(builds, params[:job_age].seconds.ago)
- end
-
- build_ids = retrieve_queue(-> { queue.execute(builds) })
-
- @metrics.observe_queue_size(-> { build_ids.size }, @runner.runner_type)
-
- build_ids.each { |build_id| yield Ci::Build.find(build_id) }
end
+
+ if runner.ref_protected?
+ builds = queue.builds_for_protected_runner(builds)
+ end
+
+ # pick builds that does not have other tags than runner's one
+ builds = queue.builds_matching_tag_ids(builds, runner.tags.ids)
+
+ # pick builds that have at least one tag
+ unless runner.run_untagged?
+ builds = queue.builds_with_any_tags(builds)
+ end
+
+ # pick builds that older than specified age
+ if params.key?(:job_age)
+ builds = queue.builds_queued_before(builds, params[:job_age].seconds.ago)
+ end
+
+ build_ids = retrieve_queue(-> { queue.execute(builds) })
+
+ @metrics.observe_queue_size(-> { build_ids.size }, @runner.runner_type)
+
+ build_ids.each { |build_id| yield Ci::Build.find(build_id) }
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
index 1d329fe7b5..dfd97498fc 100644
--- a/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
+++ b/app/services/ci/resource_groups/assign_resource_from_resource_group_service.rb
@@ -9,7 +9,7 @@ module Ci
free_resources = resource_group.resources.free.count
- resource_group.processables.waiting_for_resource.take(free_resources).each do |processable|
+ resource_group.upcoming_processables.take(free_resources).each do |processable|
processable.enqueue_waiting_for_resource
end
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index 08520c9514..07cfbb9ce3 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -17,7 +17,7 @@ module Ci
def execute(build)
build.ensure_scheduling_type!
- reprocess!(build).tap do |new_build|
+ clone!(build).tap do |new_build|
check_assignable_runners!(new_build)
next if new_build.failed?
@@ -31,7 +31,12 @@ module Ci
end
# rubocop: disable CodeReuse/ActiveRecord
- def reprocess!(build)
+ def clone!(build)
+ # Cloning a build requires a strict type check to ensure
+ # the attributes being used for the clone are taken straight
+ # from the model and not overridden by other abstractions.
+ raise TypeError unless build.instance_of?(Ci::Build)
+
check_access!(build)
new_build = clone_build(build)
diff --git a/app/services/ci/retry_pipeline_service.rb b/app/services/ci/retry_pipeline_service.rb
index 02ee40d2cf..9ad46ca758 100644
--- a/app/services/ci/retry_pipeline_service.rb
+++ b/app/services/ci/retry_pipeline_service.rb
@@ -9,20 +9,15 @@ module Ci
raise Gitlab::Access::AccessDeniedError
end
- needs = Set.new
-
pipeline.ensure_scheduling_type!
builds_relation(pipeline).find_each do |build|
next unless can_be_retried?(build)
- Ci::RetryBuildService.new(project, current_user)
- .reprocess!(build)
-
- needs += build.needs.map(&:name)
+ Ci::RetryBuildService.new(project, current_user).clone!(build)
end
- pipeline.builds.latest.skipped.find_each do |skipped|
+ pipeline.processables.latest.skipped.find_each do |skipped|
retry_optimistic_lock(skipped, name: 'ci_retry_pipeline') { |build| build.process(current_user) }
end
diff --git a/app/services/ci/stuck_builds/drop_pending_service.rb b/app/services/ci/stuck_builds/drop_pending_service.rb
new file mode 100644
index 0000000000..4653e70197
--- /dev/null
+++ b/app/services/ci/stuck_builds/drop_pending_service.rb
@@ -0,0 +1,42 @@
+# frozen_string_literal: true
+
+module Ci
+ module StuckBuilds
+ class DropPendingService
+ include DropHelpers
+
+ BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
+ BUILD_PENDING_STUCK_TIMEOUT = 1.hour
+ BUILD_LOOKBACK = 5.days
+
+ def execute
+ Gitlab::AppLogger.info "#{self.class}: Cleaning pending timed-out builds"
+
+ drop(
+ pending_builds(BUILD_PENDING_OUTDATED_TIMEOUT.ago),
+ failure_reason: :stuck_or_timeout_failure
+ )
+
+ drop_stuck(
+ pending_builds(BUILD_PENDING_STUCK_TIMEOUT.ago),
+ failure_reason: :stuck_or_timeout_failure
+ )
+ end
+
+ private
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ # We're adding the ordering clause by `created_at` and `project_id`
+ # because we want to force the query planner to use the
+ # `ci_builds_gitlab_monitor_metrics` index all the time.
+ def pending_builds(timeout)
+ if Feature.enabled?(:ci_new_query_for_pending_stuck_jobs)
+ Ci::Build.pending.created_at_before(timeout).updated_at_before(timeout).order(created_at: :asc, project_id: :asc)
+ else
+ Ci::Build.pending.updated_before(lookback: BUILD_LOOKBACK.ago, timeout: timeout)
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+ end
+end
diff --git a/app/services/ci/stuck_builds/drop_running_service.rb b/app/services/ci/stuck_builds/drop_running_service.rb
new file mode 100644
index 0000000000..a79224cc23
--- /dev/null
+++ b/app/services/ci/stuck_builds/drop_running_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Ci
+ module StuckBuilds
+ class DropRunningService
+ include DropHelpers
+
+ BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
+
+ def execute
+ Gitlab::AppLogger.info "#{self.class}: Cleaning running, timed-out builds"
+
+ drop(running_timed_out_builds, failure_reason: :stuck_or_timeout_failure)
+ end
+
+ private
+
+ def running_timed_out_builds
+ if Feature.enabled?(:ci_new_query_for_running_stuck_jobs, default_enabled: :yaml)
+ Ci::Build
+ .running
+ .created_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
+ .updated_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
+ .order(created_at: :asc, project_id: :asc) # rubocop:disable CodeReuse/ActiveRecord
+ else
+ Ci::Build.running.updated_at_before(BUILD_RUNNING_OUTDATED_TIMEOUT.ago)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/ci/stuck_builds/drop_scheduled_service.rb b/app/services/ci/stuck_builds/drop_scheduled_service.rb
new file mode 100644
index 0000000000..d4f4252c2c
--- /dev/null
+++ b/app/services/ci/stuck_builds/drop_scheduled_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Ci
+ module StuckBuilds
+ class DropScheduledService
+ include DropHelpers
+
+ BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour
+
+ def execute
+ Gitlab::AppLogger.info "#{self.class}: Cleaning scheduled, timed-out builds"
+
+ drop(scheduled_timed_out_builds, failure_reason: :stale_schedule)
+ end
+
+ private
+
+ def scheduled_timed_out_builds
+ Ci::Build.scheduled.scheduled_at_before(BUILD_SCHEDULED_OUTDATED_TIMEOUT.ago)
+ end
+ end
+ end
+end
diff --git a/app/services/ci/stuck_builds/drop_service.rb b/app/services/ci/stuck_builds/drop_service.rb
deleted file mode 100644
index 3fee9a9438..0000000000
--- a/app/services/ci/stuck_builds/drop_service.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-module Ci
- module StuckBuilds
- class DropService
- include DropHelpers
-
- BUILD_RUNNING_OUTDATED_TIMEOUT = 1.hour
- BUILD_PENDING_OUTDATED_TIMEOUT = 1.day
- BUILD_SCHEDULED_OUTDATED_TIMEOUT = 1.hour
- BUILD_PENDING_STUCK_TIMEOUT = 1.hour
- BUILD_LOOKBACK = 5.days
-
- def execute
- Gitlab::AppLogger.info "#{self.class}: Cleaning stuck builds"
-
- drop(running_timed_out_builds, failure_reason: :stuck_or_timeout_failure)
-
- drop(
- pending_builds(BUILD_PENDING_OUTDATED_TIMEOUT.ago),
- failure_reason: :stuck_or_timeout_failure
- )
-
- drop(scheduled_timed_out_builds, failure_reason: :stale_schedule)
-
- drop_stuck(
- pending_builds(BUILD_PENDING_STUCK_TIMEOUT.ago),
- failure_reason: :stuck_or_timeout_failure
- )
- end
-
- private
-
- # rubocop: disable CodeReuse/ActiveRecord
- # We're adding the ordering clause by `created_at` and `project_id`
- # because we want to force the query planner to use the
- # `ci_builds_gitlab_monitor_metrics` index all the time.
- def pending_builds(timeout)
- if Feature.enabled?(:ci_new_query_for_pending_stuck_jobs)
- Ci::Build.pending.created_at_before(timeout).updated_at_before(timeout).order(created_at: :asc, project_id: :asc)
- else
- Ci::Build.pending.updated_before(lookback: BUILD_LOOKBACK.ago, timeout: timeout)
- end
- end
- # rubocop: enable CodeReuse/ActiveRecord
-
- def scheduled_timed_out_builds
- Ci::Build.where(status: :scheduled).where( # rubocop: disable CodeReuse/ActiveRecord
- 'ci_builds.scheduled_at IS NOT NULL AND ci_builds.scheduled_at < ?',
- BUILD_SCHEDULED_OUTDATED_TIMEOUT.ago
- )
- end
-
- def running_timed_out_builds
- Ci::Build.running.where( # rubocop: disable CodeReuse/ActiveRecord
- 'ci_builds.updated_at < ?',
- BUILD_RUNNING_OUTDATED_TIMEOUT.ago
- )
- end
- end
- end
-end
diff --git a/app/services/ci/update_build_state_service.rb b/app/services/ci/update_build_state_service.rb
index abd50d2f11..3b403f9248 100644
--- a/app/services/ci/update_build_state_service.rb
+++ b/app/services/ci/update_build_state_service.rb
@@ -73,9 +73,11 @@ module Ci
::Gitlab::Ci::Trace::Checksum.new(build).then do |checksum|
unless checksum.valid?
metrics.increment_trace_operation(operation: :invalid)
+ metrics.increment_error_counter(type: :chunks_invalid_checksum)
if checksum.corrupted?
metrics.increment_trace_operation(operation: :corrupted)
+ metrics.increment_error_counter(type: :chunks_invalid_size)
end
next unless log_invalid_chunks?
diff --git a/app/services/ci/update_pending_build_service.rb b/app/services/ci/update_pending_build_service.rb
index dcba06e60b..d546dbcfe3 100644
--- a/app/services/ci/update_pending_build_service.rb
+++ b/app/services/ci/update_pending_build_service.rb
@@ -2,7 +2,7 @@
module Ci
class UpdatePendingBuildService
- VALID_PARAMS = %i[instance_runners_enabled].freeze
+ VALID_PARAMS = %i[instance_runners_enabled namespace_id namespace_traversal_ids].freeze
InvalidParamsError = Class.new(StandardError)
InvalidModelError = Class.new(StandardError)
diff --git a/app/services/clusters/agent_tokens/create_service.rb b/app/services/clusters/agent_tokens/create_service.rb
new file mode 100644
index 0000000000..ae2617f510
--- /dev/null
+++ b/app/services/clusters/agent_tokens/create_service.rb
@@ -0,0 +1,31 @@
+# frozen_string_literal: true
+
+module Clusters
+ module AgentTokens
+ class CreateService < ::BaseContainerService
+ ALLOWED_PARAMS = %i[agent_id description name].freeze
+
+ def execute
+ return error_no_permissions unless current_user.can?(:create_cluster, container)
+
+ token = ::Clusters::AgentToken.new(filtered_params.merge(created_by_user: current_user))
+
+ if token.save
+ ServiceResponse.success(payload: { secret: token.token, token: token })
+ else
+ ServiceResponse.error(message: token.errors.full_messages)
+ end
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: s_('ClusterAgent|User has insufficient permissions to create a token for this project'))
+ end
+
+ def filtered_params
+ params.slice(*ALLOWED_PARAMS)
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/create_service.rb b/app/services/clusters/agents/create_service.rb
new file mode 100644
index 0000000000..568f168d63
--- /dev/null
+++ b/app/services/clusters/agents/create_service.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class CreateService < BaseService
+ def execute(name:)
+ return error_no_permissions unless cluster_agent_permissions?
+
+ agent = ::Clusters::Agent.new(name: name, project: project, created_by_user: current_user)
+
+ if agent.save
+ success.merge(cluster_agent: agent)
+ else
+ error(agent.errors.full_messages)
+ end
+ end
+
+ private
+
+ def cluster_agent_permissions?
+ current_user.can?(:admin_pipeline, project) && current_user.can?(:create_cluster, project)
+ end
+
+ def error_no_permissions
+ error(s_('ClusterAgent|You have insufficient permissions to create a cluster agent for this project'))
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/delete_service.rb b/app/services/clusters/agents/delete_service.rb
new file mode 100644
index 0000000000..2132dffa60
--- /dev/null
+++ b/app/services/clusters/agents/delete_service.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Clusters
+ module Agents
+ class DeleteService < ::BaseContainerService
+ def execute(cluster_agent)
+ return error_no_permissions unless current_user.can?(:admin_cluster, cluster_agent)
+
+ if cluster_agent.destroy
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: cluster_agent.errors.full_messages)
+ end
+ end
+
+ private
+
+ def error_no_permissions
+ ServiceResponse.error(message: s_('ClusterAgent|You have insufficient permissions to delete this cluster agent'))
+ end
+ end
+ end
+end
diff --git a/app/services/clusters/agents/refresh_authorization_service.rb b/app/services/clusters/agents/refresh_authorization_service.rb
index a9e3340dbf..7f401eef72 100644
--- a/app/services/clusters/agents/refresh_authorization_service.rb
+++ b/app/services/clusters/agents/refresh_authorization_service.rb
@@ -99,7 +99,7 @@ module Clusters
end
def group_root_ancestor?
- root_ancestor.group?
+ root_ancestor.group_namespace?
end
end
end
diff --git a/app/services/concerns/rate_limited_service.rb b/app/services/concerns/rate_limited_service.rb
new file mode 100644
index 0000000000..87cba7814f
--- /dev/null
+++ b/app/services/concerns/rate_limited_service.rb
@@ -0,0 +1,93 @@
+# frozen_string_literal: true
+
+module RateLimitedService
+ extend ActiveSupport::Concern
+
+ RateLimitedNotSetupError = Class.new(StandardError)
+
+ class RateLimitedError < StandardError
+ def initialize(key:, rate_limiter:)
+ @key = key
+ @rate_limiter = rate_limiter
+ end
+
+ def headers
+ # TODO: This will be fleshed out in https://gitlab.com/gitlab-org/gitlab/-/issues/342370
+ {}
+ end
+
+ def log_request(request, current_user)
+ rate_limiter.class.log_request(request, "#{key}_request_limit".to_sym, current_user)
+ end
+
+ private
+
+ attr_reader :key, :rate_limiter
+ end
+
+ class RateLimiterScopedAndKeyed
+ attr_reader :key, :opts, :rate_limiter_klass
+
+ def initialize(key:, opts:, rate_limiter_klass:)
+ @key = key
+ @opts = opts
+ @rate_limiter_klass = rate_limiter_klass
+ end
+
+ def rate_limit!(service)
+ evaluated_scope = evaluated_scope_for(service)
+ return if feature_flag_disabled?(evaluated_scope[:project])
+
+ rate_limiter = new_rate_limiter(evaluated_scope)
+ if rate_limiter.throttled?
+ raise RateLimitedError.new(key: key, rate_limiter: rate_limiter), _('This endpoint has been requested too many times. Try again later.')
+ end
+ end
+
+ private
+
+ def users_allowlist
+ @users_allowlist ||= opts[:users_allowlist] ? opts[:users_allowlist].call : []
+ end
+
+ def evaluated_scope_for(service)
+ opts[:scope].each_with_object({}) do |var, all|
+ all[var] = service.public_send(var) # rubocop: disable GitlabSecurity/PublicSend
+ end
+ end
+
+ def feature_flag_disabled?(project)
+ Feature.disabled?("rate_limited_service_#{key}", project, default_enabled: :yaml)
+ end
+
+ def new_rate_limiter(evaluated_scope)
+ rate_limiter_klass.new(key, **opts.merge(scope: evaluated_scope.values, users_allowlist: users_allowlist))
+ end
+ end
+
+ prepended do
+ attr_accessor :rate_limiter_bypassed
+ cattr_accessor :rate_limiter_scoped_and_keyed
+
+ def self.rate_limit(key:, opts:, rate_limiter_klass: ::Gitlab::ApplicationRateLimiter)
+ self.rate_limiter_scoped_and_keyed = RateLimiterScopedAndKeyed.new(key: key,
+ opts: opts,
+ rate_limiter_klass: rate_limiter_klass)
+ end
+ end
+
+ def execute_without_rate_limiting(*args, **kwargs)
+ self.rate_limiter_bypassed = true
+ execute(*args, **kwargs)
+ ensure
+ self.rate_limiter_bypassed = false
+ end
+
+ def execute(*args, **kwargs)
+ raise RateLimitedNotSetupError if rate_limiter_scoped_and_keyed.nil?
+
+ rate_limiter_scoped_and_keyed.rate_limit!(self) unless rate_limiter_bypassed
+
+ super
+ end
+end
diff --git a/app/services/container_expiration_policies/cleanup_service.rb b/app/services/container_expiration_policies/cleanup_service.rb
index cd988cdc5f..0da5e552c4 100644
--- a/app/services/container_expiration_policies/cleanup_service.rb
+++ b/app/services/container_expiration_policies/cleanup_service.rb
@@ -4,7 +4,7 @@ module ContainerExpirationPolicies
class CleanupService
attr_reader :repository
- SERVICE_RESULT_FIELDS = %i[original_size before_truncate_size after_truncate_size before_delete_size deleted_size].freeze
+ SERVICE_RESULT_FIELDS = %i[original_size before_truncate_size after_truncate_size before_delete_size deleted_size cached_tags_count].freeze
def initialize(repository)
@repository = repository
@@ -24,8 +24,8 @@ module ContainerExpirationPolicies
begin
service_result = Projects::ContainerRepository::CleanupTagsService
- .new(project, nil, policy_params.merge('container_expiration_policy' => true))
- .execute(repository)
+ .new(repository, nil, policy_params.merge('container_expiration_policy' => true))
+ .execute
rescue StandardError
repository.cleanup_unfinished!
diff --git a/app/services/customer_relations/contacts/base_service.rb b/app/services/customer_relations/contacts/base_service.rb
new file mode 100644
index 0000000000..89f6f2c3f1
--- /dev/null
+++ b/app/services/customer_relations/contacts/base_service.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module CustomerRelations
+ module Contacts
+ class BaseService < ::BaseGroupService
+ private
+
+ def allowed?
+ current_user&.can?(:admin_contact, group)
+ end
+
+ def error(message)
+ ServiceResponse.error(message: Array(message))
+ end
+ end
+ end
+end
diff --git a/app/services/customer_relations/contacts/create_service.rb b/app/services/customer_relations/contacts/create_service.rb
new file mode 100644
index 0000000000..7ff8b731e0
--- /dev/null
+++ b/app/services/customer_relations/contacts/create_service.rb
@@ -0,0 +1,41 @@
+# frozen_string_literal: true
+
+module CustomerRelations
+ module Contacts
+ class CreateService < BaseService
+ def execute
+ return error_no_permissions unless allowed?
+ return error_organization_invalid unless organization_valid?
+
+ contact = Contact.create(params.merge(group_id: group.id))
+
+ return error_creating(contact) unless contact.persisted?
+
+ ServiceResponse.success(payload: contact)
+ end
+
+ private
+
+ def organization_valid?
+ return true unless params[:organization_id]
+
+ organization = Organization.find(params[:organization_id])
+ organization.group_id == group.id
+ rescue ActiveRecord::RecordNotFound
+ false
+ end
+
+ def error_organization_invalid
+ error('The specified organization was not found or does not belong to this group')
+ end
+
+ def error_no_permissions
+ error('You have insufficient permissions to create a contact for this group')
+ end
+
+ def error_creating(contact)
+ error(contact&.errors&.full_messages || 'Failed to create contact')
+ end
+ end
+ end
+end
diff --git a/app/services/customer_relations/contacts/update_service.rb b/app/services/customer_relations/contacts/update_service.rb
new file mode 100644
index 0000000000..473a80be26
--- /dev/null
+++ b/app/services/customer_relations/contacts/update_service.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+module CustomerRelations
+ module Contacts
+ class UpdateService < BaseService
+ def execute(contact)
+ return error_no_permissions unless allowed?
+ return error_updating(contact) unless contact.update(params)
+
+ ServiceResponse.success(payload: contact)
+ end
+
+ private
+
+ def error_no_permissions
+ error('You have insufficient permissions to update a contact for this group')
+ end
+
+ def error_updating(contact)
+ error(contact&.errors&.full_messages || 'Failed to update contact')
+ end
+ end
+ end
+end
diff --git a/app/services/customer_relations/organizations/base_service.rb b/app/services/customer_relations/organizations/base_service.rb
index 63261534b3..8f8480d697 100644
--- a/app/services/customer_relations/organizations/base_service.rb
+++ b/app/services/customer_relations/organizations/base_service.rb
@@ -10,7 +10,7 @@ module CustomerRelations
end
def error(message)
- ServiceResponse.error(message: message)
+ ServiceResponse.error(message: Array(message))
end
end
end
diff --git a/app/services/customer_relations/organizations/create_service.rb b/app/services/customer_relations/organizations/create_service.rb
index 9c223796ea..aad1b7e2ca 100644
--- a/app/services/customer_relations/organizations/create_service.rb
+++ b/app/services/customer_relations/organizations/create_service.rb
@@ -7,9 +7,7 @@ module CustomerRelations
def execute
return error_no_permissions unless allowed?
- params[:group_id] = group.id
-
- organization = Organization.create(params)
+ organization = Organization.create(params.merge(group_id: group.id))
return error_creating(organization) unless organization.persisted?
diff --git a/app/services/dependency_proxy/auth_token_service.rb b/app/services/dependency_proxy/auth_token_service.rb
index 16279ed12b..c6c9eb534b 100644
--- a/app/services/dependency_proxy/auth_token_service.rb
+++ b/app/services/dependency_proxy/auth_token_service.rb
@@ -12,10 +12,16 @@ module DependencyProxy
JSONWebToken::HMACToken.decode(token, ::Auth::DependencyProxyAuthenticationService.secret).first
end
- class << self
- def decoded_token_payload(token)
- self.new(token).execute
+ def self.user_or_deploy_token_from_jwt(raw_jwt)
+ token_payload = self.new(raw_jwt).execute
+
+ if token_payload['user_id']
+ User.find(token_payload['user_id'])
+ elsif token_payload['deploy_token']
+ DeployToken.active.find_by_token(token_payload['deploy_token'])
end
+ rescue JWT::DecodeError, JWT::ExpiredSignature, JWT::ImmatureSignature
+ nil
end
end
end
diff --git a/app/services/dependency_proxy/find_or_create_blob_service.rb b/app/services/dependency_proxy/find_or_create_blob_service.rb
index f3dbf31dcd..0a6db6e3d3 100644
--- a/app/services/dependency_proxy/find_or_create_blob_service.rb
+++ b/app/services/dependency_proxy/find_or_create_blob_service.rb
@@ -12,7 +12,7 @@ module DependencyProxy
def execute
from_cache = true
file_name = @blob_sha.sub('sha256:', '') + '.gz'
- blob = @group.dependency_proxy_blobs.find_or_build(file_name)
+ blob = @group.dependency_proxy_blobs.active.find_or_build(file_name)
unless blob.persisted?
from_cache = false
@@ -30,6 +30,8 @@ module DependencyProxy
blob.save!
end
+ # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
+ blob.touch if from_cache
success(blob: blob, from_cache: from_cache)
end
diff --git a/app/services/dependency_proxy/find_or_create_manifest_service.rb b/app/services/dependency_proxy/find_or_create_manifest_service.rb
index 0eb990ab7f..1976d4d47f 100644
--- a/app/services/dependency_proxy/find_or_create_manifest_service.rb
+++ b/app/services/dependency_proxy/find_or_create_manifest_service.rb
@@ -13,11 +13,16 @@ module DependencyProxy
def execute
@manifest = @group.dependency_proxy_manifests
+ .active
.find_or_initialize_by_file_name_or_digest(file_name: @file_name, digest: @tag)
head_result = DependencyProxy::HeadManifestService.new(@image, @tag, @token).execute
- return success(manifest: @manifest, from_cache: true) if cached_manifest_matches?(head_result)
+ if cached_manifest_matches?(head_result)
+ @manifest.touch
+
+ return success(manifest: @manifest, from_cache: true)
+ end
pull_new_manifest
respond(from_cache: false)
@@ -46,6 +51,9 @@ module DependencyProxy
def respond(from_cache: true)
if @manifest.persisted?
+ # Technical debt: change to read_at https://gitlab.com/gitlab-org/gitlab/-/issues/341536
+ @manifest.touch if from_cache
+
success(manifest: @manifest, from_cache: from_cache)
else
error('Failed to download the manifest from the external registry', 503)
diff --git a/app/services/dependency_proxy/group_settings/update_service.rb b/app/services/dependency_proxy/group_settings/update_service.rb
new file mode 100644
index 0000000000..ba43452def
--- /dev/null
+++ b/app/services/dependency_proxy/group_settings/update_service.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ module GroupSettings
+ class UpdateService < BaseContainerService
+ ALLOWED_ATTRIBUTES = %i[enabled].freeze
+
+ def execute
+ return ServiceResponse.error(message: 'Access Denied', http_status: 403) unless allowed?
+ return ServiceResponse.error(message: 'Dependency proxy setting not found', http_status: 404) unless dependency_proxy_setting
+
+ if dependency_proxy_setting.update(dependency_proxy_setting_params)
+ ServiceResponse.success(payload: { dependency_proxy_setting: dependency_proxy_setting })
+ else
+ ServiceResponse.error(
+ message: dependency_proxy_setting.errors.full_messages.to_sentence || 'Bad request',
+ http_status: 400
+ )
+ end
+ end
+
+ private
+
+ def dependency_proxy_setting
+ container.dependency_proxy_setting
+ end
+
+ def allowed?
+ Ability.allowed?(current_user, :admin_dependency_proxy, container)
+ end
+
+ def dependency_proxy_setting_params
+ params.slice(*ALLOWED_ATTRIBUTES)
+ end
+ end
+ end
+end
diff --git a/app/services/deployments/older_deployments_drop_service.rb b/app/services/deployments/older_deployments_drop_service.rb
index 100d126784..504b55b99a 100644
--- a/app/services/deployments/older_deployments_drop_service.rb
+++ b/app/services/deployments/older_deployments_drop_service.rb
@@ -11,23 +11,23 @@ module Deployments
def execute
return unless @deployment&.running?
- older_deployments.find_each do |older_deployment|
- Gitlab::OptimisticLocking.retry_lock(older_deployment.deployable, name: 'older_deployments_drop') do |deployable|
- deployable.drop(:forward_deployment_failure)
+ older_deployments_builds.each do |build|
+ Gitlab::OptimisticLocking.retry_lock(build, name: 'older_deployments_drop') do |build|
+ build.drop(:forward_deployment_failure)
end
rescue StandardError => e
- Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, deployment_id: older_deployment.id)
+ Gitlab::ErrorTracking.track_exception(e, subject_id: @deployment.id, build_id: build.id)
end
end
private
- def older_deployments
+ def older_deployments_builds
@deployment
.environment
.active_deployments
.older_than(@deployment)
- .with_deployable
+ .builds
end
end
end
diff --git a/app/services/error_tracking/list_issues_service.rb b/app/services/error_tracking/list_issues_service.rb
index 86c7791e75..1979816b88 100644
--- a/app/services/error_tracking/list_issues_service.rb
+++ b/app/services/error_tracking/list_issues_service.rb
@@ -76,16 +76,21 @@ module ErrorTracking
filter_opts = {
status: opts[:issue_status],
sort: opts[:sort],
- limit: opts[:limit]
+ limit: opts[:limit],
+ cursor: opts[:cursor]
}
errors = ErrorTracking::ErrorsFinder.new(current_user, project, filter_opts).execute
+ pagination = {}
+ pagination[:next] = { cursor: errors.cursor_for_next_page } if errors.has_next_page?
+ pagination[:previous] = { cursor: errors.cursor_for_previous_page } if errors.has_previous_page?
+
# We use the same response format as project_error_tracking_setting
# method below for compatibility with existing code.
{
issues: errors.map(&:to_sentry_error),
- pagination: {}
+ pagination: pagination
}
else
project_error_tracking_setting.list_sentry_issues(**opts)
diff --git a/app/services/feature_flags/base_service.rb b/app/services/feature_flags/base_service.rb
index 9ae9ab4de6..ca0b6b8919 100644
--- a/app/services/feature_flags/base_service.rb
+++ b/app/services/feature_flags/base_service.rb
@@ -7,6 +7,8 @@ module FeatureFlags
AUDITABLE_ATTRIBUTES = %w(name description active).freeze
def success(**args)
+ audit_event = args.fetch(:audit_event) { audit_event(args[:feature_flag]) }
+ save_audit_event(audit_event)
sync_to_jira(args[:feature_flag])
super
end
@@ -66,5 +68,11 @@ module FeatureFlags
feature_flag_by_name.scopes.find_by_environment_scope(params[:environment_scope])
end
end
+
+ private
+
+ def audit_message(feature_flag)
+ raise NotImplementedError, "This method should be overriden by subclasses"
+ end
end
end
diff --git a/app/services/feature_flags/create_service.rb b/app/services/feature_flags/create_service.rb
index 65f8f8e33f..ebbe71f39c 100644
--- a/app/services/feature_flags/create_service.rb
+++ b/app/services/feature_flags/create_service.rb
@@ -10,8 +10,6 @@ module FeatureFlags
feature_flag = project.operations_feature_flags.new(params)
if feature_flag.save
- save_audit_event(audit_event(feature_flag))
-
success(feature_flag: feature_flag)
else
error(feature_flag.errors.full_messages, 400)
diff --git a/app/services/feature_flags/destroy_service.rb b/app/services/feature_flags/destroy_service.rb
index 986fe004db..817a80940c 100644
--- a/app/services/feature_flags/destroy_service.rb
+++ b/app/services/feature_flags/destroy_service.rb
@@ -13,8 +13,6 @@ module FeatureFlags
ApplicationRecord.transaction do
if feature_flag.destroy
- save_audit_event(audit_event(feature_flag))
-
success(feature_flag: feature_flag)
else
error(feature_flag.errors.full_messages)
diff --git a/app/services/feature_flags/hook_service.rb b/app/services/feature_flags/hook_service.rb
new file mode 100644
index 0000000000..6f77a70bd0
--- /dev/null
+++ b/app/services/feature_flags/hook_service.rb
@@ -0,0 +1,28 @@
+# frozen_string_literal: true
+
+module FeatureFlags
+ class HookService
+ HOOK_NAME = :feature_flag_hooks
+
+ def initialize(feature_flag, current_user)
+ @feature_flag = feature_flag
+ @current_user = current_user
+ end
+
+ def execute
+ project.execute_hooks(hook_data, HOOK_NAME)
+ end
+
+ private
+
+ attr_reader :feature_flag, :current_user
+
+ def project
+ @project ||= feature_flag.project
+ end
+
+ def hook_data
+ Gitlab::DataBuilder::FeatureFlag.build(feature_flag, current_user)
+ end
+ end
+end
diff --git a/app/services/feature_flags/update_service.rb b/app/services/feature_flags/update_service.rb
index ccfd1b57d4..bcfd2c1518 100644
--- a/app/services/feature_flags/update_service.rb
+++ b/app/services/feature_flags/update_service.rb
@@ -7,6 +7,11 @@ module FeatureFlags
'parameters' => 'parameters'
}.freeze
+ def success(**args)
+ execute_hooks_after_commit(args[:feature_flag])
+ super
+ end
+
def execute(feature_flag)
return error('Access Denied', 403) unless can_update?(feature_flag)
return error('Not Found', 404) unless valid_user_list_ids?(feature_flag, user_list_ids(params))
@@ -20,16 +25,11 @@ module FeatureFlags
end
end
+ # We generate the audit event before the feature flag is saved as #changed_strategies_messages depends on the strategies' states before save
audit_event = audit_event(feature_flag)
- if feature_flag.active_changed?
- feature_flag.execute_hooks(current_user)
- end
-
if feature_flag.save
- save_audit_event(audit_event)
-
- success(feature_flag: feature_flag)
+ success(feature_flag: feature_flag, audit_event: audit_event)
else
error(feature_flag.errors.full_messages, :bad_request)
end
@@ -38,6 +38,16 @@ module FeatureFlags
private
+ def execute_hooks_after_commit(feature_flag)
+ return unless feature_flag.active_previously_changed?
+
+ # The `current_user` method (defined in `BaseService`) is not available within the `run_after_commit` block
+ user = current_user
+ feature_flag.run_after_commit do
+ HookService.new(feature_flag, user).execute
+ end
+ end
+
def audit_message(feature_flag)
changes = changed_attributes_messages(feature_flag)
changes += changed_strategies_messages(feature_flag)
diff --git a/app/services/groups/transfer_service.rb b/app/services/groups/transfer_service.rb
index d0aea848af..334083a859 100644
--- a/app/services/groups/transfer_service.rb
+++ b/app/services/groups/transfer_service.rb
@@ -29,6 +29,7 @@ module Groups
update_group_attributes
ensure_ownership
update_integrations
+ update_pending_builds!
end
post_update_hooks(@updated_project_ids)
@@ -139,6 +140,10 @@ module Groups
# these records again.
@updated_project_ids = projects_to_update.pluck(:id)
+ Namespaces::ProjectNamespace
+ .where(id: projects_to_update.select(:project_namespace_id))
+ .update_all(visibility_level: @new_parent_group.visibility_level)
+
projects_to_update
.update_all(visibility_level: @new_parent_group.visibility_level)
end
@@ -225,6 +230,15 @@ module Groups
PropagateIntegrationWorker.perform_async(integration.id)
end
end
+
+ def update_pending_builds!
+ update_params = {
+ namespace_traversal_ids: group.traversal_ids,
+ namespace_id: group.id
+ }
+
+ ::Ci::UpdatePendingBuildService.new(group, update_params).execute
+ end
end
end
diff --git a/app/services/import/validate_remote_git_endpoint_service.rb b/app/services/import/validate_remote_git_endpoint_service.rb
new file mode 100644
index 0000000000..afccb5373a
--- /dev/null
+++ b/app/services/import/validate_remote_git_endpoint_service.rb
@@ -0,0 +1,76 @@
+# frozen_string_literal: true
+
+module Import
+ class ValidateRemoteGitEndpointService
+ # Validates if the remote endpoint is a valid GIT repository
+ # Only smart protocol is supported
+ # Validation rules are taken from https://git-scm.com/docs/http-protocol#_smart_clients
+
+ GIT_SERVICE_NAME = "git-upload-pack"
+ GIT_EXPECTED_FIRST_PACKET_LINE = "# service=#{GIT_SERVICE_NAME}"
+ GIT_BODY_MESSAGE_REGEXP = /^[0-9a-f]{4}#{GIT_EXPECTED_FIRST_PACKET_LINE}/.freeze
+ # https://github.com/git/git/blob/master/Documentation/technical/protocol-common.txt#L56-L59
+ GIT_PROTOCOL_PKT_LEN = 4
+ GIT_MINIMUM_RESPONSE_LENGTH = GIT_PROTOCOL_PKT_LEN + GIT_EXPECTED_FIRST_PACKET_LINE.length
+ EXPECTED_CONTENT_TYPE = "application/x-#{GIT_SERVICE_NAME}-advertisement"
+
+ def initialize(params)
+ @params = params
+ end
+
+ def execute
+ uri = Gitlab::Utils.parse_url(@params[:url])
+
+ return ServiceResponse.error(message: "#{@params[:url]} is not a valid URL") unless uri
+
+ uri.fragment = nil
+ url = Gitlab::Utils.append_path(uri.to_s, "/info/refs?service=#{GIT_SERVICE_NAME}")
+
+ response_body = ''
+ result = nil
+ Gitlab::HTTP.try_get(url, stream_body: true, follow_redirects: false, basic_auth: auth) do |fragment|
+ response_body += fragment
+ next if response_body.length < GIT_MINIMUM_RESPONSE_LENGTH
+
+ result = if status_code_is_valid(fragment) && content_type_is_valid(fragment) && response_body_is_valid(response_body)
+ :success
+ else
+ :error
+ end
+
+ # We are interested only in the first chunks of the response
+ # So we're using stream_body: true and breaking when receive enough body
+ break
+ end
+
+ if result == :success
+ ServiceResponse.success
+ else
+ ServiceResponse.error(message: "#{uri} is not a valid HTTP Git repository")
+ end
+ end
+
+ private
+
+ def auth
+ unless @params[:user].to_s.blank?
+ {
+ username: @params[:user],
+ password: @params[:password]
+ }
+ end
+ end
+
+ def status_code_is_valid(fragment)
+ fragment.http_response.code == '200'
+ end
+
+ def content_type_is_valid(fragment)
+ fragment.http_response['content-type'] == EXPECTED_CONTENT_TYPE
+ end
+
+ def response_body_is_valid(response_body)
+ response_body.match?(GIT_BODY_MESSAGE_REGEXP)
+ end
+ end
+end
diff --git a/app/services/issuable/import_csv/base_service.rb b/app/services/issuable/import_csv/base_service.rb
index 4a6b7540de..4a2078a4e6 100644
--- a/app/services/issuable/import_csv/base_service.rb
+++ b/app/services/issuable/import_csv/base_service.rb
@@ -71,7 +71,14 @@ module Issuable
# NOTE: CSV imports are performed by workers, so we do not have a request context in order
# to create a SpamParams object to pass to the issuable create service.
spam_params = nil
- create_issuable_class.new(project: @project, current_user: @user, params: attributes, spam_params: spam_params).execute
+ create_service = create_issuable_class.new(project: @project, current_user: @user, params: attributes, spam_params: spam_params)
+
+ # For now, if create_issuable_class prepends RateLimitedService let's bypass rate limiting
+ if create_issuable_class < RateLimitedService
+ create_service.execute_without_rate_limiting
+ else
+ create_service.execute
+ end
end
def email_results_to_user
diff --git a/app/services/issues/clone_service.rb b/app/services/issues/clone_service.rb
index cb42334fe3..c675f957cd 100644
--- a/app/services/issues/clone_service.rb
+++ b/app/services/issues/clone_service.rb
@@ -8,13 +8,7 @@ module Issues
@target_project = target_project
@with_notes = with_notes
- unless issue.can_clone?(current_user, target_project)
- raise CloneError, s_('CloneIssue|Cannot clone issue due to insufficient permissions!')
- end
-
- if target_project.pending_delete?
- raise CloneError, s_('CloneIssue|Cannot clone issue to target project as it is pending deletion.')
- end
+ verify_can_clone_issue!(issue, target_project)
super(issue, target_project)
@@ -30,6 +24,20 @@ module Issues
attr_reader :target_project
attr_reader :with_notes
+ def verify_can_clone_issue!(issue, target_project)
+ unless issue.supports_move_and_clone?
+ raise CloneError, s_('CloneIssue|Cannot clone issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type }
+ end
+
+ unless issue.can_clone?(current_user, target_project)
+ raise CloneError, s_('CloneIssue|Cannot clone issue due to insufficient permissions!')
+ end
+
+ if target_project.pending_delete?
+ raise CloneError, s_('CloneIssue|Cannot clone issue to target project as it is pending deletion.')
+ end
+ end
+
def update_new_entity
# we don't call `super` because we want to be able to decide whether or not to copy all comments over.
update_new_entity_description
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index ea64239dd9..ac846c769a 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -3,8 +3,8 @@
module Issues
class CloseService < Issues::BaseService
# Closes the supplied issue if the current user is able to do so.
- def execute(issue, commit: nil, notifications: true, system_note: true)
- return issue unless can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
+ def execute(issue, commit: nil, notifications: true, system_note: true, skip_authorization: false)
+ return issue unless can_close?(issue, skip_authorization: skip_authorization)
close_issue(issue,
closed_via: commit,
@@ -24,7 +24,7 @@ module Issues
return issue
end
- if project.issues_enabled? && issue.close(current_user)
+ if perform_close(issue)
event_service.close_issue(issue, current_user)
create_note(issue, closed_via) if system_note
@@ -51,6 +51,15 @@ module Issues
private
+ # Overridden on EE
+ def perform_close(issue)
+ issue.close(current_user)
+ end
+
+ def can_close?(issue, skip_authorization: false)
+ skip_authorization || can?(current_user, :update_issue, issue) || issue.is_a?(ExternalIssue)
+ end
+
def perform_incident_management_actions(issue)
resolve_alert(issue)
end
@@ -82,11 +91,11 @@ module Issues
end
end
- def store_first_mentioned_in_commit_at(issue, merge_request)
+ def store_first_mentioned_in_commit_at(issue, merge_request, max_commit_lookup: 100)
metrics = issue.metrics
return if metrics.nil? || metrics.first_mentioned_in_commit_at
- first_commit_timestamp = merge_request.commits(limit: 1).first.try(:authored_date)
+ first_commit_timestamp = merge_request.commits(limit: max_commit_lookup).last.try(:authored_date)
return unless first_commit_timestamp
metrics.update!(first_mentioned_in_commit_at: first_commit_timestamp)
diff --git a/app/services/issues/create_service.rb b/app/services/issues/create_service.rb
index b15b3e49c9..fcedd1c1c8 100644
--- a/app/services/issues/create_service.rb
+++ b/app/services/issues/create_service.rb
@@ -3,6 +3,10 @@
module Issues
class CreateService < Issues::BaseService
include ResolveDiscussions
+ prepend RateLimitedService
+
+ rate_limit key: :issues_create,
+ opts: { scope: [:project, :current_user], users_allowlist: -> { [User.support_bot.username] } }
# NOTE: For Issues::CreateService, we require the spam_params and do not default it to nil, because
# spam_checking is likely to be necessary. However, if there is not a request available in scope
diff --git a/app/services/issues/move_service.rb b/app/services/issues/move_service.rb
index ff78221c94..4418b4eb2b 100644
--- a/app/services/issues/move_service.rb
+++ b/app/services/issues/move_service.rb
@@ -7,13 +7,7 @@ module Issues
def execute(issue, target_project)
@target_project = target_project
- unless issue.can_move?(current_user, @target_project)
- raise MoveError, s_('MoveIssue|Cannot move issue due to insufficient permissions!')
- end
-
- if @project == @target_project
- raise MoveError, s_('MoveIssue|Cannot move issue to project it originates from!')
- end
+ verify_can_move_issue!(issue, target_project)
super
@@ -32,6 +26,20 @@ module Issues
attr_reader :target_project
+ def verify_can_move_issue!(issue, target_project)
+ unless issue.supports_move_and_clone?
+ raise MoveError, s_('MoveIssue|Cannot move issues of \'%{issue_type}\' type.') % { issue_type: issue.issue_type }
+ end
+
+ unless issue.can_move?(current_user, @target_project)
+ raise MoveError, s_('MoveIssue|Cannot move issue due to insufficient permissions!')
+ end
+
+ if @project == @target_project
+ raise MoveError, s_('MoveIssue|Cannot move issue to project it originates from!')
+ end
+ end
+
def update_service_desk_sent_notifications
return unless original_entity.from_service_desk?
diff --git a/app/services/issues/relative_position_rebalancing_service.rb b/app/services/issues/relative_position_rebalancing_service.rb
index 7d199f99a2..23bb409f3c 100644
--- a/app/services/issues/relative_position_rebalancing_service.rb
+++ b/app/services/issues/relative_position_rebalancing_service.rb
@@ -82,7 +82,7 @@ module Issues
collection.each do |project|
caching.cache_current_project_id(project.id)
index += 1
- scope = Issue.in_projects(project).reorder(custom_reorder).select(:id, :relative_position)
+ scope = Issue.in_projects(project).order_by_relative_position.with_non_null_relative_position.select(:id, :relative_position)
with_retry(PREFETCH_ISSUES_BATCH_SIZE, 100) do |batch_size|
Gitlab::Pagination::Keyset::Iterator.new(scope: scope).each_batch(of: batch_size) do |batch|
@@ -166,10 +166,6 @@ module Issues
@start_position ||= (RelativePositioning::START_POSITION - (gaps / 2) * gap_size).to_i
end
- def custom_reorder
- ::Gitlab::Pagination::Keyset::Order.build([Issue.column_order_relative_position, Issue.column_order_id_asc])
- end
-
def with_retry(initial_batch_size, exit_batch_size)
retries = 0
batch_size = initial_batch_size
diff --git a/app/services/issues/reopen_service.rb b/app/services/issues/reopen_service.rb
index 977b924ed7..4abd1dfbf4 100644
--- a/app/services/issues/reopen_service.rb
+++ b/app/services/issues/reopen_service.rb
@@ -2,10 +2,10 @@
module Issues
class ReopenService < Issues::BaseService
- def execute(issue)
- return issue unless can?(current_user, :reopen_issue, issue)
+ def execute(issue, skip_authorization: false)
+ return issue unless can_reopen?(issue, skip_authorization: skip_authorization)
- if issue.reopen
+ if perform_reopen(issue)
event_service.reopen_issue(issue, current_user)
create_note(issue, 'reopened')
notification_service.async.reopen_issue(issue, current_user)
@@ -22,6 +22,15 @@ module Issues
private
+ # Overriden on EE
+ def perform_reopen(issue)
+ issue.reopen
+ end
+
+ def can_reopen?(issue, skip_authorization: false)
+ skip_authorization || can?(current_user, :reopen_issue, issue)
+ end
+
def perform_incident_management_actions(issue)
end
diff --git a/app/services/merge_requests/mergeability/check_base_service.rb b/app/services/merge_requests/mergeability/check_base_service.rb
new file mode 100644
index 0000000000..d5ddcb4b82
--- /dev/null
+++ b/app/services/merge_requests/mergeability/check_base_service.rb
@@ -0,0 +1,36 @@
+# frozen_string_literal: true
+module MergeRequests
+ module Mergeability
+ class CheckBaseService
+ attr_reader :merge_request, :params
+
+ def initialize(merge_request:, params:)
+ @merge_request = merge_request
+ @params = params
+ end
+
+ def skip?
+ raise NotImplementedError
+ end
+
+ # When this method is true, we need to implement a cache_key
+ def cacheable?
+ raise NotImplementedError
+ end
+
+ def cache_key
+ raise NotImplementedError
+ end
+
+ private
+
+ def success(*args)
+ Gitlab::MergeRequests::Mergeability::CheckResult.success(*args)
+ end
+
+ def failure(*args)
+ Gitlab::MergeRequests::Mergeability::CheckResult.failed(*args)
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/mergeability/check_ci_status_service.rb b/app/services/merge_requests/mergeability/check_ci_status_service.rb
new file mode 100644
index 0000000000..c0ef5ba1c3
--- /dev/null
+++ b/app/services/merge_requests/mergeability/check_ci_status_service.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+module MergeRequests
+ module Mergeability
+ class CheckCiStatusService < CheckBaseService
+ def execute
+ if merge_request.mergeable_ci_state?
+ success
+ else
+ failure
+ end
+ end
+
+ def skip?
+ params[:skip_ci_check].present?
+ end
+
+ def cacheable?
+ false
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/mergeability/run_checks_service.rb b/app/services/merge_requests/mergeability/run_checks_service.rb
new file mode 100644
index 0000000000..c1d65fb65c
--- /dev/null
+++ b/app/services/merge_requests/mergeability/run_checks_service.rb
@@ -0,0 +1,54 @@
+# frozen_string_literal: true
+module MergeRequests
+ module Mergeability
+ class RunChecksService
+ include Gitlab::Utils::StrongMemoize
+
+ # We want to have the cheapest checks first in the list,
+ # that way we can fail fast before running the more expensive ones
+ CHECKS = [
+ CheckCiStatusService
+ ].freeze
+
+ def initialize(merge_request:, params:)
+ @merge_request = merge_request
+ @params = params
+ end
+
+ def execute
+ CHECKS.each_with_object([]) do |check_class, results|
+ check = check_class.new(merge_request: merge_request, params: params)
+
+ next if check.skip?
+
+ check_result = run_check(check)
+ results << check_result
+
+ break results if check_result.failed?
+ end
+ end
+
+ private
+
+ attr_reader :merge_request, :params
+
+ def run_check(check)
+ return check.execute unless Feature.enabled?(:mergeability_caching, merge_request.project, default_enabled: :yaml)
+ return check.execute unless check.cacheable?
+
+ cached_result = results.read(merge_check: check)
+ return cached_result if cached_result.respond_to?(:status)
+
+ check.execute.tap do |result|
+ results.write(merge_check: check, result_hash: result.to_hash)
+ end
+ end
+
+ def results
+ strong_memoize(:results) do
+ Gitlab::MergeRequests::Mergeability::ResultsStore.new(merge_request: merge_request)
+ end
+ end
+ end
+ end
+end
diff --git a/app/services/merge_requests/update_service.rb b/app/services/merge_requests/update_service.rb
index af041de559..c539513890 100644
--- a/app/services/merge_requests/update_service.rb
+++ b/app/services/merge_requests/update_service.rb
@@ -248,7 +248,7 @@ module MergeRequests
def merge_from_quick_action(merge_request)
last_diff_sha = params.delete(:merge)
- MergeRequests::MergeOrchestrationService
+ ::MergeRequests::MergeOrchestrationService
.new(project, current_user, { sha: last_diff_sha })
.execute(merge_request)
end
diff --git a/app/services/metrics/dashboard/annotations/create_service.rb b/app/services/metrics/dashboard/annotations/create_service.rb
index 54f4e96378..b86fa82a5e 100644
--- a/app/services/metrics/dashboard/annotations/create_service.rb
+++ b/app/services/metrics/dashboard/annotations/create_service.rb
@@ -30,7 +30,7 @@ module Metrics
options[:environment] = environment
success(options)
else
- error(s_('Metrics::Dashboard::Annotation|You are not authorized to create annotation for selected environment'))
+ error(s_('MetricsDashboardAnnotation|You are not authorized to create annotation for selected environment'))
end
end
@@ -39,7 +39,7 @@ module Metrics
options[:cluster] = cluster
success(options)
else
- error(s_('Metrics::Dashboard::Annotation|You are not authorized to create annotation for selected cluster'))
+ error(s_('MetricsDashboardAnnotation|You are not authorized to create annotation for selected cluster'))
end
end
@@ -51,7 +51,7 @@ module Metrics
success(options)
rescue Gitlab::Template::Finders::RepoTemplateFinder::FileNotFoundError
- error(s_('Metrics::Dashboard::Annotation|Dashboard with requested path can not be found'))
+ error(s_('MetricsDashboardAnnotation|Dashboard with requested path can not be found'))
end
def create(options)
diff --git a/app/services/metrics/dashboard/annotations/delete_service.rb b/app/services/metrics/dashboard/annotations/delete_service.rb
index 3efe6924a9..3cb22f8d3d 100644
--- a/app/services/metrics/dashboard/annotations/delete_service.rb
+++ b/app/services/metrics/dashboard/annotations/delete_service.rb
@@ -27,7 +27,7 @@ module Metrics
if Ability.allowed?(user, :delete_metrics_dashboard_annotation, annotation)
success
else
- error(s_('Metrics::Dashboard::Annotation|You are not authorized to delete this annotation'))
+ error(s_('MetricsDashboardAnnotation|You are not authorized to delete this annotation'))
end
end
@@ -35,7 +35,7 @@ module Metrics
if annotation.destroy
success
else
- error(s_('Metrics::Dashboard::Annotation|Annotation has not been deleted'))
+ error(s_('MetricsDashboardAnnotation|Annotation has not been deleted'))
end
end
end
diff --git a/app/services/metrics/users_starred_dashboards/create_service.rb b/app/services/metrics/users_starred_dashboards/create_service.rb
index 9642df8786..0d028f120d 100644
--- a/app/services/metrics/users_starred_dashboards/create_service.rb
+++ b/app/services/metrics/users_starred_dashboards/create_service.rb
@@ -35,7 +35,7 @@ module Metrics
if Ability.allowed?(user, :create_metrics_user_starred_dashboard, project)
success(user: user, project: project)
else
- error(s_('Metrics::UsersStarredDashboards|You are not authorized to add star to this dashboard'))
+ error(s_('MetricsUsersStarredDashboards|You are not authorized to add star to this dashboard'))
end
end
@@ -44,7 +44,7 @@ module Metrics
options[:dashboard_path] = dashboard_path
success(options)
else
- error(s_('Metrics::UsersStarredDashboards|Dashboard with requested path can not be found'))
+ error(s_('MetricsUsersStarredDashboards|Dashboard with requested path can not be found'))
end
end
diff --git a/app/services/packages/composer/create_package_service.rb b/app/services/packages/composer/create_package_service.rb
index 8215a3385a..0f5429f667 100644
--- a/app/services/packages/composer/create_package_service.rb
+++ b/app/services/packages/composer/create_package_service.rb
@@ -17,10 +17,6 @@ module Packages
})
end
- unless Feature.enabled?(:remove_composer_v1_cache_code, project)
- ::Packages::Composer::CacheUpdateWorker.perform_async(created_package.project_id, created_package.name, nil)
- end
-
created_package
end
diff --git a/app/services/projects/after_rename_service.rb b/app/services/projects/after_rename_service.rb
index 953b386b75..a3d54bc6b5 100644
--- a/app/services/projects/after_rename_service.rb
+++ b/app/services/projects/after_rename_service.rb
@@ -12,6 +12,8 @@ module Projects
#
# Projects::AfterRenameService.new(project).execute
class AfterRenameService
+ include BaseServiceUtility
+
# @return [String] The Project being renamed.
attr_reader :project
@@ -78,7 +80,7 @@ module Projects
def execute_system_hooks
project.old_path_with_namespace = full_path_before
- SystemHooksService.new.execute_hooks_for(project, :rename)
+ system_hook_service.execute_hooks_for(project, :rename)
end
def update_repository_configuration
@@ -110,7 +112,7 @@ module Projects
end
def log_completion
- Gitlab::AppLogger.info(
+ log_info(
"Project #{project.id} has been renamed from " \
"#{full_path_before} to #{full_path_after}"
)
@@ -140,7 +142,7 @@ module Projects
def rename_failed!
error = "Repository #{full_path_before} could not be renamed to #{full_path_after}"
- Gitlab::AppLogger.error(error)
+ log_error(error)
raise RenameFailedError, error
end
diff --git a/app/services/projects/container_repository/cache_tags_created_at_service.rb b/app/services/projects/container_repository/cache_tags_created_at_service.rb
new file mode 100644
index 0000000000..3a5346d7a2
--- /dev/null
+++ b/app/services/projects/container_repository/cache_tags_created_at_service.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module Projects
+ module ContainerRepository
+ class CacheTagsCreatedAtService
+ def initialize(container_repository)
+ @container_repository = container_repository
+ @cached_tag_names = Set.new
+ end
+
+ def populate(tags)
+ return if tags.empty?
+
+ # This will load all tags in one Redis roundtrip
+ # the maximum number of tags is configurable and is set to 200 by default.
+ # https://gitlab.com/gitlab-org/gitlab/blob/master/doc/user/packages/container_registry/index.md#set-cleanup-limits-to-conserve-resources
+ keys = tags.map(&method(:cache_key))
+ cached_tags_count = 0
+
+ ::Gitlab::Redis::Cache.with do |redis|
+ tags.zip(redis.mget(keys)).each do |tag, created_at|
+ next unless created_at
+
+ tag.created_at = DateTime.rfc3339(created_at)
+ @cached_tag_names << tag.name
+ cached_tags_count += 1
+ end
+ end
+
+ cached_tags_count
+ end
+
+ def insert(tags, max_ttl_in_seconds)
+ return unless max_ttl_in_seconds
+ return if tags.empty?
+
+ # tags with nil created_at are not cacheable
+ # tags already cached don't need to be cached again
+ cacheable_tags = tags.select do |tag|
+ tag.created_at.present? && !tag.name.in?(@cached_tag_names)
+ end
+
+ return if cacheable_tags.empty?
+
+ now = Time.zone.now
+
+ ::Gitlab::Redis::Cache.with do |redis|
+ # we use a pipeline instead of a MSET because each tag has
+ # a specific ttl
+ redis.pipelined do
+ cacheable_tags.each do |tag|
+ created_at = tag.created_at
+ # ttl is the max_ttl_in_seconds reduced by the number
+ # of seconds that the tag has already existed
+ ttl = max_ttl_in_seconds - (now - created_at).seconds
+ ttl = ttl.to_i
+ redis.set(cache_key(tag), created_at.rfc3339, ex: ttl) if ttl > 0
+ end
+ end
+ end
+ end
+
+ private
+
+ def cache_key(tag)
+ "container_repository:{#{@container_repository.id}}:tag:#{tag.name}:created_at"
+ end
+ end
+ end
+end
diff --git a/app/services/projects/container_repository/cleanup_tags_service.rb b/app/services/projects/container_repository/cleanup_tags_service.rb
index 793d2fec03..3a60de0f1e 100644
--- a/app/services/projects/container_repository/cleanup_tags_service.rb
+++ b/app/services/projects/container_repository/cleanup_tags_service.rb
@@ -2,116 +2,152 @@
module Projects
module ContainerRepository
- class CleanupTagsService < BaseService
- def execute(container_repository)
+ class CleanupTagsService
+ include BaseServiceUtility
+ include ::Gitlab::Utils::StrongMemoize
+
+ def initialize(container_repository, user = nil, params = {})
+ @container_repository = container_repository
+ @current_user = user
+ @params = params.dup
+
+ @project = container_repository.project
+ @tags = container_repository.tags
+ tags_size = @tags.size
+ @counts = {
+ original_size: tags_size,
+ cached_tags_count: 0
+ }
+ end
+
+ def execute
return error('access denied') unless can_destroy?
return error('invalid regex') unless valid_regex?
- tags = container_repository.tags
- original_size = tags.size
+ filter_out_latest
+ filter_by_name
- tags = without_latest(tags)
- tags = filter_by_name(tags)
+ truncate
+ populate_from_cache
- before_truncate_size = tags.size
- tags = truncate(tags)
- after_truncate_size = tags.size
+ filter_keep_n
+ filter_by_older_than
- tags = filter_keep_n(tags)
- tags = filter_by_older_than(tags)
-
- delete_tags(container_repository, tags).tap do |result|
- result[:original_size] = original_size
- result[:before_truncate_size] = before_truncate_size
- result[:after_truncate_size] = after_truncate_size
- result[:before_delete_size] = tags.size
+ delete_tags.merge(@counts).tap do |result|
+ result[:before_delete_size] = @tags.size
result[:deleted_size] = result[:deleted]&.size
- result[:status] = :error if before_truncate_size != after_truncate_size
+ result[:status] = :error if @counts[:before_truncate_size] != @counts[:after_truncate_size]
end
end
private
- def delete_tags(container_repository, tags)
- return success(deleted: []) unless tags.any?
-
- tag_names = tags.map(&:name)
+ def delete_tags
+ return success(deleted: []) unless @tags.any?
service = Projects::ContainerRepository::DeleteTagsService.new(
- container_repository.project,
- current_user,
- tags: tag_names,
- container_expiration_policy: params['container_expiration_policy']
+ @project,
+ @current_user,
+ tags: @tags.map(&:name),
+ container_expiration_policy: container_expiration_policy
)
- service.execute(container_repository)
+ service.execute(@container_repository)
end
- def without_latest(tags)
- tags.reject(&:latest?)
+ def filter_out_latest
+ @tags.reject!(&:latest?)
end
- def order_by_date(tags)
+ def order_by_date
now = DateTime.current
- tags.sort_by { |tag| tag.created_at || now }.reverse
+ @tags.sort_by! { |tag| tag.created_at || now }
+ .reverse!
end
- def filter_by_name(tags)
- regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_delete'] || params['name_regex']}\\z")
- regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{params['name_regex_keep']}\\z")
+ def filter_by_name
+ regex_delete = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_delete || name_regex}\\z")
+ regex_retain = ::Gitlab::UntrustedRegexp.new("\\A#{name_regex_keep}\\z")
- tags.select do |tag|
+ @tags.select! do |tag|
# regex_retain will override any overlapping matches by regex_delete
regex_delete.match?(tag.name) && !regex_retain.match?(tag.name)
end
end
- def filter_keep_n(tags)
- return tags unless params['keep_n']
+ def filter_keep_n
+ return unless keep_n
- tags = order_by_date(tags)
- tags.drop(keep_n)
+ order_by_date
+ cache_tags(@tags.first(keep_n_as_integer))
+ @tags = @tags.drop(keep_n_as_integer)
end
- def filter_by_older_than(tags)
- return tags unless params['older_than']
+ def filter_by_older_than
+ return unless older_than
- older_than = ChronicDuration.parse(params['older_than']).seconds.ago
+ older_than_timestamp = older_than_in_seconds.ago
- tags.select do |tag|
- tag.created_at && tag.created_at < older_than
+ @tags, tags_to_keep = @tags.partition do |tag|
+ tag.created_at && tag.created_at < older_than_timestamp
end
+
+ cache_tags(tags_to_keep)
end
def can_destroy?
- return true if params['container_expiration_policy']
+ return true if container_expiration_policy
- can?(current_user, :destroy_container_image, project)
+ can?(@current_user, :destroy_container_image, @project)
end
def valid_regex?
%w(name_regex_delete name_regex name_regex_keep).each do |param_name|
- regex = params[param_name]
+ regex = @params[param_name]
::Gitlab::UntrustedRegexp.new(regex) unless regex.blank?
end
true
rescue RegexpError => e
- ::Gitlab::ErrorTracking.log_exception(e, project_id: project.id)
+ ::Gitlab::ErrorTracking.log_exception(e, project_id: @project.id)
false
end
- def truncate(tags)
- return tags unless throttling_enabled?
- return tags if max_list_size == 0
+ def truncate
+ @counts[:before_truncate_size] = @tags.size
+ @counts[:after_truncate_size] = @tags.size
+
+ return unless throttling_enabled?
+ return if max_list_size == 0
# truncate the list to make sure that after the #filter_keep_n
# execution, the resulting list will be max_list_size
- truncated_size = max_list_size + keep_n
+ truncated_size = max_list_size + keep_n_as_integer
- return tags if tags.size <= truncated_size
+ return if @tags.size <= truncated_size
- tags.sample(truncated_size)
+ @tags = @tags.sample(truncated_size)
+ @counts[:after_truncate_size] = @tags.size
+ end
+
+ def populate_from_cache
+ @counts[:cached_tags_count] = cache.populate(@tags) if caching_enabled?
+ end
+
+ def cache_tags(tags)
+ cache.insert(tags, older_than_in_seconds) if caching_enabled?
+ end
+
+ def cache
+ strong_memoize(:cache) do
+ ::Projects::ContainerRepository::CacheTagsCreatedAtService.new(@container_repository)
+ end
+ end
+
+ def caching_enabled?
+ container_expiration_policy &&
+ older_than.present? &&
+ Feature.enabled?(:container_registry_expiration_policies_caching, @project)
end
def throttling_enabled?
@@ -123,7 +159,37 @@ module Projects
end
def keep_n
- params['keep_n'].to_i
+ @params['keep_n']
+ end
+
+ def keep_n_as_integer
+ keep_n.to_i
+ end
+
+ def older_than_in_seconds
+ strong_memoize(:older_than_in_seconds) do
+ ChronicDuration.parse(older_than).seconds
+ end
+ end
+
+ def older_than
+ @params['older_than']
+ end
+
+ def name_regex_delete
+ @params['name_regex_delete']
+ end
+
+ def name_regex
+ @params['name_regex']
+ end
+
+ def name_regex_keep
+ @params['name_regex_keep']
+ end
+
+ def container_expiration_policy
+ @params['container_expiration_policy']
end
end
end
diff --git a/app/services/projects/create_service.rb b/app/services/projects/create_service.rb
index e717491b19..1536f0a22b 100644
--- a/app/services/projects/create_service.rb
+++ b/app/services/projects/create_service.rb
@@ -8,6 +8,7 @@ module Projects
@current_user = user
@params = params.dup
@skip_wiki = @params.delete(:skip_wiki)
+ @initialize_with_sast = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_sast))
@initialize_with_readme = Gitlab::Utils.to_boolean(@params.delete(:initialize_with_readme))
@import_data = @params.delete(:import_data)
@relations_block = @params.delete(:relations_block)
@@ -118,6 +119,7 @@ module Projects
Projects::PostCreationWorker.perform_async(@project.id)
create_readme if @initialize_with_readme
+ create_sast_commit if @initialize_with_sast
end
# Add an authorization for the current user authorizations inline
@@ -160,6 +162,10 @@ module Projects
Files::CreateService.new(@project, current_user, commit_attrs).execute
end
+ def create_sast_commit
+ ::Security::CiConfiguration::SastCreateService.new(@project, current_user, {}, commit_on_default: true).execute
+ end
+
def readme_content
@readme_template.presence || experiment(:new_project_readme_content, namespace: @project.namespace).run_with(@project)
end
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index afa8de04fc..27f813f466 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -5,6 +5,7 @@ module Projects
include Gitlab::ShellAdapter
DestroyError = Class.new(StandardError)
+ BATCH_SIZE = 100
def async_execute
project.update_attribute(:pending_delete, true)
@@ -119,6 +120,12 @@ module Projects
destroy_web_hooks!
destroy_project_bots!
+ if ::Feature.enabled?(:ci_optimize_project_records_destruction, project, default_enabled: :yaml) &&
+ Feature.enabled?(:abort_deleted_project_pipelines, default_enabled: :yaml)
+
+ destroy_ci_records!
+ end
+
# Rails attempts to load all related records into memory before
# destroying: https://github.com/rails/rails/issues/22510
# This ensures we delete records in batches.
@@ -133,6 +140,23 @@ module Projects
log_info("Attempting to destroy #{project.full_path} (#{project.id})")
end
+ def destroy_ci_records!
+ project.all_pipelines.find_each(batch_size: BATCH_SIZE) do |pipeline| # rubocop: disable CodeReuse/ActiveRecord
+ # Destroy artifacts, then builds, then pipelines
+ # All builds have already been dropped by Ci::AbortPipelinesService,
+ # so no Ci::Build-instantiating cancellations happen here.
+ # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71342#note_691523196
+
+ ::Ci::DestroyPipelineService.new(project, current_user).execute(pipeline)
+ end
+
+ deleted_count = project.commit_statuses.delete_all
+
+ if deleted_count > 0
+ Gitlab::AppLogger.info "Projects::DestroyService - Project #{project.id} - #{deleted_count} leftover commit statuses"
+ end
+ end
+
# The project can have multiple webhooks with hundreds of thousands of web_hook_logs.
# By default, they are removed with "DELETE CASCADE" option defined via foreign_key.
# But such queries can exceed the statement_timeout limit and fail to delete the project.
diff --git a/app/services/projects/group_links/update_service.rb b/app/services/projects/group_links/update_service.rb
index 475ab17f1a..a836b96cac 100644
--- a/app/services/projects/group_links/update_service.rb
+++ b/app/services/projects/group_links/update_service.rb
@@ -20,19 +20,15 @@ module Projects
attr_reader :group_link
def refresh_authorizations
- if Feature.enabled?(:specialized_worker_for_project_share_update_auth_recalculation)
- AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
- # Until we compare the inconsistency rates of the new specialized worker and
- # the old approach, we still run AuthorizedProjectsWorker
- # but with some delay and lower urgency as a safety net.
- group_link.group.refresh_members_authorized_projects(
- blocking: false,
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- else
- group_link.group.refresh_members_authorized_projects
- end
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ group_link.group.refresh_members_authorized_projects(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
end
def requires_authorization_refresh?(params)
diff --git a/app/services/projects/import_service.rb b/app/services/projects/import_service.rb
index b5288aad6f..4979af6dfe 100644
--- a/app/services/projects/import_service.rb
+++ b/app/services/projects/import_service.rb
@@ -16,6 +16,8 @@ module Projects
end
def execute
+ track_start_import
+
add_repository_to_project
download_lfs_objects
@@ -25,16 +27,17 @@ module Projects
after_execute_hook
success
- rescue Gitlab::UrlBlocker::BlockedUrlError => e
- Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
+ rescue Gitlab::UrlBlocker::BlockedUrlError, StandardError => e
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: self.class.name,
+ exception: e,
+ metrics: true
+ )
- error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: e.message })
- rescue StandardError => e
message = Projects::ImportErrorFilter.filter_message(e.message)
-
- Gitlab::ErrorTracking.track_exception(e, project_path: project.full_path, importer: project.import_type)
-
- error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") % { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: message })
+ error(s_("ImportProjects|Error importing repository %{project_safe_import_url} into %{project_full_path} - %{message}") %
+ { project_safe_import_url: project.safe_import_url, project_full_path: project.full_path, message: message })
end
protected
@@ -54,6 +57,10 @@ module Projects
# Defined in EE::Projects::ImportService
end
+ def track_start_import
+ has_importer? && importer_class.try(:track_start_import, project)
+ end
+
def add_repository_to_project
if project.external_import? && !unknown_url?
begin
diff --git a/app/services/projects/overwrite_project_service.rb b/app/services/projects/overwrite_project_service.rb
index f35370c427..2612001eb9 100644
--- a/app/services/projects/overwrite_project_service.rb
+++ b/app/services/projects/overwrite_project_service.rb
@@ -3,7 +3,7 @@
module Projects
class OverwriteProjectService < BaseService
def execute(source_project)
- return unless source_project && source_project.namespace == @project.namespace
+ return unless source_project && source_project.namespace_id == @project.namespace_id
start_time = ::Gitlab::Metrics::System.monotonic_time
@@ -40,7 +40,7 @@ module Projects
duration = ::Gitlab::Metrics::System.monotonic_time - start_time
Gitlab::AppJsonLogger.info(class: self.class.name,
- namespace_id: source_project.namespace.id,
+ namespace_id: source_project.namespace_id,
project_id: source_project.id,
duration_s: duration.to_f,
error: exception.class.name)
diff --git a/app/services/projects/participants_service.rb b/app/services/projects/participants_service.rb
index 228115d72b..1616a8a406 100644
--- a/app/services/projects/participants_service.rb
+++ b/app/services/projects/participants_service.rb
@@ -36,14 +36,17 @@ module Projects
private
def project_members_through_invited_groups
- groups_with_ancestors_ids = Gitlab::ObjectHierarchy
- .new(visible_groups)
- .base_and_ancestors
- .pluck_primary_key
+ groups_with_ancestors = if ::Feature.enabled?(:linear_participants_service_ancestor_scopes, current_user, default_enabled: :yaml)
+ visible_groups.self_and_ancestors
+ else
+ Gitlab::ObjectHierarchy
+ .new(visible_groups)
+ .base_and_ancestors
+ end
GroupMember
.active_without_invites_and_requests
- .with_source_id(groups_with_ancestors_ids)
+ .with_source_id(groups_with_ancestors.pluck_primary_key)
end
def visible_groups
diff --git a/app/services/projects/transfer_service.rb b/app/services/projects/transfer_service.rb
index 27376173f0..a69e6488eb 100644
--- a/app/services/projects/transfer_service.rb
+++ b/app/services/projects/transfer_service.rb
@@ -81,7 +81,7 @@ module Projects
# Apply changes to the project
update_namespace_and_visibility(@new_namespace)
- update_shared_runners_settings
+ project.reconcile_shared_runners_setting!
project.save!
# Notifications
@@ -104,6 +104,8 @@ module Projects
update_repository_configuration(@new_path)
execute_system_hooks
+
+ update_pending_builds!
end
post_update_hooks(project)
@@ -154,19 +156,15 @@ module Projects
user_ids = @old_namespace.user_ids_for_project_authorizations |
@new_namespace.user_ids_for_project_authorizations
- if Feature.enabled?(:specialized_worker_for_project_transfer_auth_recalculation)
- AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
+ AuthorizedProjectUpdate::ProjectRecalculateWorker.perform_async(project.id)
- # Until we compare the inconsistency rates of the new specialized worker and
- # the old approach, we still run AuthorizedProjectsWorker
- # but with some delay and lower urgency as a safety net.
- UserProjectAccessChangedService.new(user_ids).execute(
- blocking: false,
- priority: UserProjectAccessChangedService::LOW_PRIORITY
- )
- else
- UserProjectAccessChangedService.new(user_ids).execute
- end
+ # Until we compare the inconsistency rates of the new specialized worker and
+ # the old approach, we still run AuthorizedProjectsWorker
+ # but with some delay and lower urgency as a safety net.
+ UserProjectAccessChangedService.new(user_ids).execute(
+ blocking: false,
+ priority: UserProjectAccessChangedService::LOW_PRIORITY
+ )
end
def rollback_side_effects
@@ -189,7 +187,7 @@ module Projects
end
def execute_system_hooks
- SystemHooksService.new.execute_hooks_for(project, :transfer)
+ system_hook_service.execute_hooks_for(project, :transfer)
end
def move_project_folders(project)
@@ -241,18 +239,19 @@ module Projects
"#{new_path}#{::Gitlab::GlRepository::DESIGN.path_suffix}"
end
- def update_shared_runners_settings
- # If a project is being transferred to another group it means it can already
- # have shared runners enabled but we need to check whether the new group allows that.
- if project.group && project.group.shared_runners_setting == 'disabled_and_unoverridable'
- project.shared_runners_enabled = false
- end
- end
-
def update_integrations
project.integrations.with_default_settings.delete_all
Integration.create_from_active_default_integrations(project, :project_id)
end
+
+ def update_pending_builds!
+ update_params = {
+ namespace_id: new_namespace.id,
+ namespace_traversal_ids: new_namespace.traversal_ids
+ }
+
+ ::Ci::UpdatePendingBuildService.new(project, update_params).execute
+ end
end
end
diff --git a/app/services/projects/update_pages_service.rb b/app/services/projects/update_pages_service.rb
index dc75fe1014..0000e713cb 100644
--- a/app/services/projects/update_pages_service.rb
+++ b/app/services/projects/update_pages_service.rb
@@ -136,13 +136,11 @@ module Projects
def validate_outdated_sha!
return if latest?
- if Feature.enabled?(:pages_smart_check_outdated_sha, project, default_enabled: :yaml)
- # use pipeline_id in case the build is retried
- last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
+ # use pipeline_id in case the build is retried
+ last_deployed_pipeline_id = project.pages_metadatum&.pages_deployment&.ci_build&.pipeline_id
- return unless last_deployed_pipeline_id
- return if last_deployed_pipeline_id <= build.pipeline_id
- end
+ return unless last_deployed_pipeline_id
+ return if last_deployed_pipeline_id <= build.pipeline_id
raise InvalidStateError, 'build SHA is outdated for this ref'
end
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index a89db1e9fe..b34ecf06e5 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -105,7 +105,7 @@ module Projects
end
update_pages_config if changing_pages_related_config?
- update_pending_builds if shared_runners_toggled?
+ update_pending_builds if runners_settings_toggled?
end
def after_rename_service(project)
@@ -181,13 +181,36 @@ module Projects
end
def update_pending_builds
- update_params = { instance_runners_enabled: project.shared_runners_enabled }
+ update_params = {
+ instance_runners_enabled: project.shared_runners_enabled?,
+ namespace_traversal_ids: group_runner_traversal_ids
+ }
- ::Ci::UpdatePendingBuildService.new(project, update_params).execute
+ ::Ci::UpdatePendingBuildService
+ .new(project, update_params)
+ .execute
end
- def shared_runners_toggled?
- project.previous_changes.include?('shared_runners_enabled')
+ def shared_runners_settings_toggled?
+ project.previous_changes.include?(:shared_runners_enabled)
+ end
+
+ def group_runners_settings_toggled?
+ return false unless project.ci_cd_settings.present?
+
+ project.ci_cd_settings.previous_changes.include?(:group_runners_enabled)
+ end
+
+ def runners_settings_toggled?
+ shared_runners_settings_toggled? || group_runners_settings_toggled?
+ end
+
+ def group_runner_traversal_ids
+ if project.group_runners_enabled?
+ project.namespace.traversal_ids
+ else
+ []
+ end
end
end
end
diff --git a/app/services/search/global_service.rb b/app/services/search/global_service.rb
index 33faf2d669..cee59360b4 100644
--- a/app/services/search/global_service.rb
+++ b/app/services/search/global_service.rb
@@ -24,7 +24,7 @@ module Search
# rubocop: disable CodeReuse/ActiveRecord
def projects
- @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute.preload(:topics, :taggings)
+ @projects ||= ProjectsFinder.new(params: { non_archived: true }, current_user: current_user).execute.preload(:topics, :project_topics)
end
def allowed_scopes
diff --git a/app/services/security/ci_configuration/base_create_service.rb b/app/services/security/ci_configuration/base_create_service.rb
index adb45244ad..ea77cd98ba 100644
--- a/app/services/security/ci_configuration/base_create_service.rb
+++ b/app/services/security/ci_configuration/base_create_service.rb
@@ -25,7 +25,7 @@ module Security
rescue Gitlab::Git::PreReceiveError => e
ServiceResponse.error(message: e.message)
rescue StandardError
- project.repository.rm_branch(current_user, branch_name) if project.repository.branch_exists?(branch_name)
+ remove_branch_on_exception
raise
end
@@ -50,6 +50,10 @@ module Security
Gitlab::Routing.url_helpers.project_new_merge_request_url(project, merge_request: merge_request_params)
end
+ def remove_branch_on_exception
+ project.repository.rm_branch(current_user, branch_name) if project.repository.branch_exists?(branch_name)
+ end
+
def track_event(attributes_for_commit)
action = attributes_for_commit[:actions].first
diff --git a/app/services/security/ci_configuration/sast_create_service.rb b/app/services/security/ci_configuration/sast_create_service.rb
index f495cac18f..47e01847b1 100644
--- a/app/services/security/ci_configuration/sast_create_service.rb
+++ b/app/services/security/ci_configuration/sast_create_service.rb
@@ -5,15 +5,28 @@ module Security
class SastCreateService < ::Security::CiConfiguration::BaseCreateService
attr_reader :params
- def initialize(project, current_user, params)
+ def initialize(project, current_user, params, commit_on_default: false)
super(project, current_user)
@params = params
+
+ @commit_on_default = commit_on_default
+ @branch_name = project.default_branch if @commit_on_default
end
private
+ def remove_branch_on_exception
+ super unless @commit_on_default
+ end
+
def action
- Security::CiConfiguration::SastBuildAction.new(project.auto_devops_enabled?, params, existing_gitlab_ci_content).generate
+ existing_content = begin
+ existing_gitlab_ci_content # this can fail on the very first commit
+ rescue StandardError
+ nil
+ end
+
+ Security::CiConfiguration::SastBuildAction.new(project.auto_devops_enabled?, params, existing_content).generate
end
def next_branch
diff --git a/app/services/service_ping/submit_service.rb b/app/services/service_ping/submit_service.rb
index 3417ce4f58..63e01603d4 100644
--- a/app/services/service_ping/submit_service.rb
+++ b/app/services/service_ping/submit_service.rb
@@ -78,7 +78,7 @@ module ServicePing
def store_metrics(response)
metrics = response['conv_index'] || response['dev_ops_score'] # leaving dev_ops_score here, as the response data comes from the gitlab-version-com
- return unless metrics.present?
+ return unless metrics.except('usage_data_id').present?
DevOpsReport::Metric.create!(
metrics.slice(*METRICS)
diff --git a/app/services/terraform/remote_state_handler.rb b/app/services/terraform/remote_state_handler.rb
index e9a13cee76..f13477b8b3 100644
--- a/app/services/terraform/remote_state_handler.rb
+++ b/app/services/terraform/remote_state_handler.rb
@@ -2,8 +2,6 @@
module Terraform
class RemoteStateHandler < BaseService
- include Gitlab::OptimisticLocking
-
StateLockedError = Class.new(StandardError)
UnauthorizedError = Class.new(StandardError)
@@ -60,7 +58,7 @@ module Terraform
private
def retrieve_with_lock(find_only: false)
- create_or_find!(find_only: find_only).tap { |state| retry_optimistic_lock(state, name: 'terraform_remote_state_handler_retrieve') { |state| yield state } }
+ create_or_find!(find_only: find_only).tap { |state| state.with_lock { yield state } }
end
def create_or_find!(find_only:)
diff --git a/app/services/user_project_access_changed_service.rb b/app/services/user_project_access_changed_service.rb
index 5f48f410bf..5bba986f4a 100644
--- a/app/services/user_project_access_changed_service.rb
+++ b/app/services/user_project_access_changed_service.rb
@@ -30,7 +30,7 @@ class UserProjectAccessChangedService
end
end
- ::Gitlab::Database::LoadBalancing::Sticking.bulk_stick(:user, @user_ids)
+ ::User.sticking.bulk_stick(:user, @user_ids)
result
end
diff --git a/app/services/users/update_service.rb b/app/services/users/update_service.rb
index 23c67231a2..c3df9b153a 100644
--- a/app/services/users/update_service.rb
+++ b/app/services/users/update_service.rb
@@ -5,15 +5,18 @@ module Users
include NewUserNotifier
attr_reader :user, :identity_params
+ ATTRS_REQUIRING_PASSWORD_CHECK = %w[email].freeze
+
def initialize(current_user, params = {})
@current_user = current_user
+ @validation_password = params.delete(:validation_password)
@user = params.delete(:user)
@status_params = params.delete(:status)
@identity_params = params.slice(*identity_attributes)
@params = params.dup
end
- def execute(validate: true, &block)
+ def execute(validate: true, check_password: false, &block)
yield(@user) if block_given?
user_exists = @user.persisted?
@@ -21,6 +24,11 @@ module Users
discard_read_only_attributes
assign_attributes
+
+ if check_password && require_password_check? && !@user.valid_password?(@validation_password)
+ return error(s_("Profiles|Invalid password"))
+ end
+
assign_identity
build_canonical_email
@@ -32,8 +40,8 @@ module Users
end
end
- def execute!(*args, &block)
- result = execute(*args, &block)
+ def execute!(*args, **kargs, &block)
+ result = execute(*args, **kargs, &block)
raise ActiveRecord::RecordInvalid, @user unless result[:status] == :success
@@ -42,6 +50,14 @@ module Users
private
+ def require_password_check?
+ return false unless @user.persisted?
+ return false if @user.password_automatically_set?
+
+ changes = @user.changed
+ ATTRS_REQUIRING_PASSWORD_CHECK.any? { |param| changes.include?(param) }
+ end
+
def build_canonical_email
return unless @user.email_changed?
diff --git a/app/services/users/upsert_credit_card_validation_service.rb b/app/services/users/upsert_credit_card_validation_service.rb
index 70a96b3ec6..86b5b92341 100644
--- a/app/services/users/upsert_credit_card_validation_service.rb
+++ b/app/services/users/upsert_credit_card_validation_service.rb
@@ -7,6 +7,14 @@ module Users
end
def execute
+ @params = {
+ user_id: params.fetch(:user_id),
+ credit_card_validated_at: params.fetch(:credit_card_validated_at),
+ expiration_date: get_expiration_date(params),
+ last_digits: Integer(params.fetch(:credit_card_mask_number), 10),
+ holder_name: params.fetch(:credit_card_holder_name)
+ }
+
::Users::CreditCardValidation.upsert(@params)
ServiceResponse.success(message: 'CreditCardValidation was set')
@@ -16,5 +24,14 @@ module Users
Gitlab::ErrorTracking.track_exception(e, params: @params, class: self.class.to_s)
ServiceResponse.error(message: "Could not set CreditCardValidation: #{e.message}")
end
+
+ private
+
+ def get_expiration_date(params)
+ year = params.fetch(:credit_card_expiration_year)
+ month = params.fetch(:credit_card_expiration_month)
+
+ Date.new(year, month, -1) # last day of the month
+ end
end
end
diff --git a/app/uploaders/dependency_proxy/file_uploader.rb b/app/uploaders/dependency_proxy/file_uploader.rb
index 5154f18045..f0222d4cf0 100644
--- a/app/uploaders/dependency_proxy/file_uploader.rb
+++ b/app/uploaders/dependency_proxy/file_uploader.rb
@@ -1,6 +1,7 @@
# frozen_string_literal: true
class DependencyProxy::FileUploader < GitlabUploader
+ extend Workhorse::UploadPath
include ObjectStorage::Concern
before :cache, :set_content_type
diff --git a/app/views/admin/application_settings/_abuse.html.haml b/app/views/admin/application_settings/_abuse.html.haml
index fab3ce584f..96fb848b56 100644
--- a/app/views/admin/application_settings/_abuse.html.haml
+++ b/app/views/admin/application_settings/_abuse.html.haml
@@ -5,7 +5,5 @@
.form-group
= f.label :abuse_notification_email, _('Abuse reports notification email'), class: 'label-bold'
= f.text_field :abuse_notification_email, class: 'form-control gl-form-input'
- .form-text.text-muted
- = _('Abuse reports will be sent to this address if it is set. Abuse reports are always available in the admin area.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index eb30efabb9..19c38d7be6 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -23,11 +23,11 @@
.form-group
= f.label :max_import_size, _('Maximum import size (MB)'), class: 'label-light'
= f.number_field :max_import_size, class: 'form-control gl-form-input qa-receive-max-import-size-field', title: _('Maximum size of import files.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted= _('0 for unlimited, only effective with remote storage enabled.')
+ %span.form-text.text-muted= _('Only effective when remote storage is enabled. Set to 0 for no size limit.')
.form-group
= f.label :session_expire_delay, _('Session duration (minutes)'), class: 'label-light'
= f.number_field :session_expire_delay, class: 'form-control gl-form-input', title: _('Maximum duration of a session.'), data: { toggle: 'tooltip', container: 'body' }
- %span.form-text.text-muted#session_expire_delay_help_block= _('GitLab restart is required to apply changes.')
+ %span.form-text.text-muted#session_expire_delay_help_block= _('Restart GitLab to apply changes.')
= render_if_exists 'admin/application_settings/git_two_factor_session_expiry', form: f
= render_if_exists 'admin/application_settings/personal_access_token_expiration_policy', form: f
@@ -45,13 +45,13 @@
.form-check
= f.check_box :user_default_external, class: 'form-check-input'
= f.label :user_default_external, class: 'form-check-label' do
- = _('Newly registered users will by default be external')
+ = _('Newly-registered users are external by default')
.gl-mt-3
= _('Internal users')
= f.text_field :user_default_internal_regex, placeholder: _('Regex pattern'), class: 'form-control gl-form-input gl-mt-2'
.help-block
- = _('Specify an e-mail address regex pattern to identify default internal users.')
- = link_to _('More information'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'),
+ = _('Specify an email address regex pattern to identify default internal users.')
+ = link_to _('Learn more'), help_page_path('user/permissions', anchor: 'setting-new-users-to-external'),
target: '_blank'
- unless Gitlab.com?
.form-group
@@ -59,11 +59,13 @@
.form-check
= f.check_box :deactivate_dormant_users, class: 'form-check-input'
= f.label :deactivate_dormant_users, class: 'form-check-label' do
- = _('Deactivate dormant users after 90 days of inactivity. Users can return to active status by signing in to their account. While inactive, a user is not counted as an active user in the instance.')
- = link_to _('More information'), help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users'), target: '_blank'
+ = _('Deactivate dormant users after 90 days of inactivity')
+ .help-block
+ = _('Users can reactivate their account by signing in.')
+ = link_to _('Learn more'), help_page_path('user/admin_area/moderate_users', anchor: 'automatically-deactivate-dormant-users'), target: '_blank'
.form-group
= f.label :personal_access_token_prefix, _('Personal Access Token prefix'), class: 'label-light'
- = f.text_field :personal_access_token_prefix, placeholder: _('Max 20 characters'), class: 'form-control gl-form-input'
+ = f.text_field :personal_access_token_prefix, placeholder: _('Maximum 20 characters'), class: 'form-control gl-form-input'
.form-group
= f.label :user_show_add_ssh_key_message, _('Prompt users to upload SSH keys'), class: 'label-bold'
.form-check
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index fea116bd41..8026ec4702 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -69,5 +69,12 @@
%p.form-text.text-muted
= _("The default CI/CD configuration file and path for new projects.").html_safe
= link_to sprite_icon('question-o'), help_page_path('ci/pipelines/settings', anchor: 'specify-a-custom-cicd-configuration-file'), target: '_blank'
+ .form-group
+ .form-check
+ = f.check_box :suggest_pipeline_enabled, class: 'form-check-input'
+ = f.label :suggest_pipeline_enabled, class: 'form-check-label' do
+ = s_('AdminSettings|Enable pipeline suggestion banner')
+ .form-text.text-muted
+ = s_('AdminSettings|Display a banner on merge requests in projects with no pipelines to initiate steps to add a .gitlab-ci.yml file.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_files_limits.html.haml b/app/views/admin/application_settings/_files_limits.html.haml
deleted file mode 100644
index 9cd12fa1ca..0000000000
--- a/app/views/admin/application_settings/_files_limits.html.haml
+++ /dev/null
@@ -1,34 +0,0 @@
-= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-files-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %fieldset
- %legend.h5.gl-border-none
- = _('Unauthenticated API request rate limit')
- .form-group
- = f.gitlab_ui_checkbox_component :throttle_unauthenticated_files_api_enabled,
- _('Enable unauthenticated API request rate limit'),
- help_text: _('Helps reduce request volume (e.g. from crawlers or abusive bots)'),
- checkbox_options: { data: { qa_selector: 'throttle_unauthenticated_files_api_checkbox' } }
- .form-group
- = f.label :throttle_unauthenticated_files_api_requests_per_period, 'Max unauthenticated API requests per period per IP', class: 'label-bold'
- = f.number_field :throttle_unauthenticated_files_api_requests_per_period, class: 'form-control gl-form-input'
- .form-group
- = f.label :throttle_unauthenticated_files_api_period_in_seconds, 'Unauthenticated API rate limit period in seconds', class: 'label-bold'
- = f.number_field :throttle_unauthenticated_files_api_period_in_seconds, class: 'form-control gl-form-input'
-
- %fieldset
- %legend.h5.gl-border-none
- = _('Authenticated API request rate limit')
- .form-group
- = f.gitlab_ui_checkbox_component :throttle_authenticated_files_api_enabled,
- _('Enable authenticated API request rate limit'),
- help_text: _('Helps reduce request volume (e.g. from crawlers or abusive bots)'),
- checkbox_options: { data: { qa_selector: 'throttle_authenticated_files_api_checkbox' } }
- .form-group
- = f.label :throttle_authenticated_files_api_requests_per_period, 'Max authenticated API requests per period per user', class: 'label-bold'
- = f.number_field :throttle_authenticated_files_api_requests_per_period, class: 'form-control gl-form-input'
- .form-group
- = f.label :throttle_authenticated_files_api_period_in_seconds, 'Authenticated API rate limit period in seconds', class: 'label-bold'
- = f.number_field :throttle_authenticated_files_api_period_in_seconds, class: 'form-control gl-form-input'
-
- = f.submit 'Save changes', class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_help_page.html.haml b/app/views/admin/application_settings/_help_page.html.haml
index ecf3203df9..cd7eaa1896 100644
--- a/app/views/admin/application_settings/_help_page.html.haml
+++ b/app/views/admin/application_settings/_help_page.html.haml
@@ -18,11 +18,10 @@
= f.text_field :help_page_support_url, class: 'form-control gl-form-input', placeholder: 'https://company.example.com/getting-help', :'aria-describedby' => 'support_help_block'
%span.form-text.text-muted#support_help_block= _('Alternate support URL for Help page and Help dropdown.')
- - if show_documentation_base_url_field?
- .form-group
- = f.label :help_page_documentation_base_url, _('Documentation pages URL'), class: 'label-bold'
- = f.text_field :help_page_documentation_base_url, class: 'form-control gl-form-input', placeholder: 'https://docs.gitlab.com'
- - docs_link_url = help_page_path('user/admin_area/settings/help_page', anchor: 'destination-requirements')
- - docs_link_start = ''.html_safe % { url: docs_link_url }
- %span.form-text.text-muted#support_help_block= html_escape(_('Requests for pages at %{code_start}%{help_text_url}%{code_end} redirect to the URL. The destination must meet certain requirements. %{docs_link_start}Learn more.%{docs_link_end}')) % { code_start: ''.html_safe, help_text_url: help_url, code_end: '
'.html_safe, docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
+ .form-group
+ = f.label :help_page_documentation_base_url, _('Documentation pages URL'), class: 'gl-font-weight-bold'
+ = f.text_field :help_page_documentation_base_url, class: 'form-control gl-form-input', placeholder: 'https://docs.gitlab.com'
+ - docs_link_url = help_page_path('user/admin_area/settings/help_page', anchor: 'destination-requirements')
+ - docs_link_start = ''.html_safe % { url: docs_link_url }
+ %span.form-text.text-muted#support_help_block= html_escape(_('Requests for pages at %{code_start}%{help_text_url}%{code_end} redirect to the URL. The destination must meet certain requirements. %{docs_link_start}Learn more.%{docs_link_end}')) % { code_start: ''.html_safe, help_text_url: help_url, code_end: '
'.html_safe, docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_mailgun.html.haml b/app/views/admin/application_settings/_mailgun.html.haml
index 40b4d5cac6..ad9e84ffda 100644
--- a/app/views/admin/application_settings/_mailgun.html.haml
+++ b/app/views/admin/application_settings/_mailgun.html.haml
@@ -6,7 +6,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank') }
+ = _('Configure the %{link} integration.').html_safe % { link: link_to(_('Mailgun events'), 'https://documentation.mailgun.com/en/latest/user_manual.html#webhooks', target: '_blank', rel: 'noopener noreferrer') }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-mailgun-settings'), html: { class: 'fieldset-form', id: 'mailgun-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_network_rate_limits.html.haml b/app/views/admin/application_settings/_network_rate_limits.html.haml
new file mode 100644
index 0000000000..f1857a9749
--- /dev/null
+++ b/app/views/admin/application_settings/_network_rate_limits.html.haml
@@ -0,0 +1,33 @@
+= gitlab_ui_form_for @application_setting, url: network_admin_application_settings_path(anchor: anchor), html: { class: 'fieldset-form' } do |f|
+ = form_errors(@application_setting)
+
+ %fieldset
+ = _("Rate limits can help reduce request volume (like from crawlers or abusive bots).")
+
+ %fieldset
+ .form-group
+ = f.gitlab_ui_checkbox_component :"throttle_unauthenticated_#{setting_fragment}_enabled",
+ _('Enable unauthenticated API request rate limit'),
+ checkbox_options: { data: { qa_selector: "throttle_unauthenticated_#{setting_fragment}_checkbox" } },
+ label_options: { class: 'label-bold' }
+ .form-group
+ = f.label :"throttle_unauthenticated_#{setting_fragment}_requests_per_period", _('Maximum unauthenticated API requests per rate limit period per IP'), class: 'label-bold'
+ = f.number_field :"throttle_unauthenticated_#{setting_fragment}_requests_per_period", class: 'form-control gl-form-input'
+ .form-group
+ = f.label :"throttle_unauthenticated_#{setting_fragment}_period_in_seconds", _('Unauthenticated API rate limit period in seconds'), class: 'label-bold'
+ = f.number_field :"throttle_unauthenticated_#{setting_fragment}_period_in_seconds", class: 'form-control gl-form-input'
+
+ %fieldset
+ .form-group
+ = f.gitlab_ui_checkbox_component :"throttle_authenticated_#{setting_fragment}_enabled",
+ _('Enable authenticated API request rate limit'),
+ checkbox_options: { data: { qa_selector: "throttle_authenticated_#{setting_fragment}_checkbox" } },
+ label_options: { class: 'label-bold' }
+ .form-group
+ = f.label :"throttle_authenticated_#{setting_fragment}_requests_per_period", _('Maximum authenticated API requests per rate limit period per user'), class: 'label-bold'
+ = f.number_field :"throttle_authenticated_#{setting_fragment}_requests_per_period", class: 'form-control gl-form-input'
+ .form-group
+ = f.label :"throttle_authenticated_#{setting_fragment}_period_in_seconds", _('Authenticated API rate limit period in seconds'), class: 'label-bold'
+ = f.number_field :"throttle_authenticated_#{setting_fragment}_period_in_seconds", class: 'form-control gl-form-input'
+
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_package_registry_limits.html.haml b/app/views/admin/application_settings/_package_registry_limits.html.haml
deleted file mode 100644
index 8769171c9e..0000000000
--- a/app/views/admin/application_settings/_package_registry_limits.html.haml
+++ /dev/null
@@ -1,32 +0,0 @@
-= form_for @application_setting, url: network_admin_application_settings_path(anchor: 'js-packages-limits-settings'), html: { class: 'fieldset-form' } do |f|
- = form_errors(@application_setting)
-
- %fieldset
- = _("The package registry rate limits can help reduce request volume (like from crawlers or abusive bots).")
-
- %fieldset
- .form-group
- .form-check
- = f.check_box :throttle_unauthenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_unauthenticated_packages_api_checkbox' }
- = f.label :throttle_unauthenticated_packages_api_enabled, class: 'form-check-label label-bold' do
- = _('Enable unauthenticated API request rate limit')
- .form-group
- = f.label :throttle_unauthenticated_packages_api_requests_per_period, _('Maximum unauthenticated API requests per rate limit period per IP'), class: 'label-bold'
- = f.number_field :throttle_unauthenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
- .form-group
- = f.label :throttle_unauthenticated_packages_api_period_in_seconds, _('Unauthenticated API rate limit period in seconds'), class: 'label-bold'
- = f.number_field :throttle_unauthenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
- %hr
- .form-group
- .form-check
- = f.check_box :throttle_authenticated_packages_api_enabled, class: 'form-check-input', data: { qa_selector: 'throttle_authenticated_packages_api_checkbox' }
- = f.label :throttle_authenticated_packages_api_enabled, class: 'form-check-label label-bold' do
- = _('Enable authenticated API request rate limit')
- .form-group
- = f.label :throttle_authenticated_packages_api_requests_per_period, _('Maximum authenticated API requests per rate limit period per user'), class: 'label-bold'
- = f.number_field :throttle_authenticated_packages_api_requests_per_period, class: 'form-control gl-form-input'
- .form-group
- = f.label :throttle_authenticated_packages_api_period_in_seconds, _('Authenticated API rate limit period in seconds'), class: 'label-bold'
- = f.number_field :throttle_authenticated_packages_api_period_in_seconds, class: 'form-control gl-form-input'
-
- = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
diff --git a/app/views/admin/application_settings/_performance.html.haml b/app/views/admin/application_settings/_performance.html.haml
index 50fc11ec7f..82e56cf8b8 100644
--- a/app/views/admin/application_settings/_performance.html.haml
+++ b/app/views/admin/application_settings/_performance.html.haml
@@ -6,29 +6,24 @@
.form-check
= f.check_box :authorized_keys_enabled, class: 'form-check-input'
= f.label :authorized_keys_enabled, class: 'form-check-label' do
- = _('Write to "authorized_keys" file')
+ = _('Use authorized_keys file to authenticate SSH keys')
.form-text.text-muted
- By default, we write to the "authorized_keys" file to support Git
- over SSH without additional configuration. GitLab can be optimized
- to authenticate SSH keys via the database file. Only uncheck this
- if you have configured your OpenSSH server to use the
- AuthorizedKeysCommand. Click on the help icon for more details.
- = link_to sprite_icon('question-o'), help_page_path('administration/operations/fast_ssh_key_lookup')
-
+ = _('Authenticate user SSH keys without requiring additional configuration. Performance of GitLab can be improved by using the GitLab database instead.')
+ = link_to _('How do I configure authentication using the GitLab database?'), help_page_path('administration/operations/fast_ssh_key_lookup'), target: '_blank', rel: 'noopener noreferrer'
.form-group
= f.label :raw_blob_request_limit, _('Raw blob request rate limit per minute'), class: 'label-bold'
= f.number_field :raw_blob_request_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Highest number of requests per minute for each raw path, default to 300. To disable throttling set to 0.')
+ = _('Maximum number of requests per minute for each raw path (default is 300). Set to 0 to disable throttling.')
.form-group
= f.label :push_event_hooks_limit, class: 'label-bold'
= f.number_field :push_event_hooks_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("Number of changes (branches or tags) in a single push to determine whether webhooks and services will be fired or not. Webhooks and services won't be submitted if it surpasses that value.")
+ = _('Maximum number of changes (branches or tags) in a single push for which webhooks and services trigger (default is 3).')
.form-group
= f.label :push_event_activities_limit, class: 'label-bold'
= f.number_field :push_event_activities_limit, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Number of changes (branches or tags) in a single push to determine whether individual push events or bulk push event will be created. Bulk push event will be created if it surpasses that value.')
+ = _('Threshold number of changes (branches or tags) in a single push above which a bulk push event is created (default is 3).')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_snowplow.html.haml b/app/views/admin/application_settings/_snowplow.html.haml
index 8c98778147..756c0e770a 100644
--- a/app/views/admin/application_settings/_snowplow.html.haml
+++ b/app/views/admin/application_settings/_snowplow.html.haml
@@ -7,7 +7,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
- link_start = ''.html_safe % { url: help_page_path('development/snowplow/index') }
- = html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank').html_safe, link_start: link_start, link_end: ''.html_safe }
+ = html_escape(_('Configure %{link} to track events. %{link_start}Learn more.%{link_end}')) % { link: link_to('Snowplow', 'https://snowplowanalytics.com/', target: '_blank', rel: 'noopener noreferrer').html_safe, link_start: link_start, link_end: ''.html_safe }
.settings-content
= form_for @application_setting, url: general_admin_application_settings_path(anchor: 'js-snowplow-settings'), html: { class: 'fieldset-form', id: 'snowplow-settings' } do |f|
= form_errors(@application_setting) if expanded
diff --git a/app/views/admin/application_settings/_spam.html.haml b/app/views/admin/application_settings/_spam.html.haml
index 011bce3ca9..53ca4d4aa7 100644
--- a/app/views/admin/application_settings/_spam.html.haml
+++ b/app/views/admin/application_settings/_spam.html.haml
@@ -2,6 +2,11 @@
= form_errors(@application_setting)
%fieldset
+ %h5
+ = _('reCAPTCHA')
+ %p
+ = _('reCAPTCHA helps prevent credential stuffing.')
+ = link_to _('Only reCAPTCHA v2 is supported:'), 'https://developers.google.com/recaptcha/docs/versions', target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
= f.check_box :recaptcha_enabled, class: 'form-check-input'
@@ -9,25 +14,31 @@
= _("Enable reCAPTCHA")
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from creating accounts.')
+ = link_to _('How do I configure it?'), help_page_path('integration/recaptcha.md'), target: '_blank', rel: 'noopener noreferrer'
.form-group
.form-check
= f.check_box :login_recaptcha_protection_enabled, class: 'form-check-input'
= f.label :login_recaptcha_protection_enabled, class: 'form-check-label' do
- = _("Enable reCAPTCHA for login")
+ = _('Enable reCAPTCHA for login.')
%span.form-text.text-muted#recaptcha_help_block
= _('Helps prevent bots from brute-force attacks.')
.form-group
- = f.label :recaptcha_site_key, _('reCAPTCHA Site Key'), class: 'label-bold'
+ = f.label :recaptcha_site_key, _('reCAPTCHA site key'), class: 'label-bold'
= f.text_field :recaptcha_site_key, class: 'form-control gl-form-input'
.form-text.text-muted
= _("Generate site and private keys at")
%a{ href: 'http://www.google.com/recaptcha', target: 'blank' } http://www.google.com/recaptcha
.form-group
- = f.label :recaptcha_private_key, _('reCAPTCHA Private Key'), class: 'label-bold'
- .form-group
+ = f.label :recaptcha_private_key, _('reCAPTCHA private key'), class: 'label-bold'
= f.text_field :recaptcha_private_key, class: 'form-control gl-form-input'
+ %h5
+ = _('Invisible Captcha')
+ %p
+ = _('Invisible Captcha helps prevent the creation of spam accounts. It adds a honeypot field and time-sensitive form submission to the account signup form.')
+ = link_to _('Read their documentation.'), 'https://github.com/markets/invisible_captcha', target: '_blank', rel: 'noopener noreferrer'
+
.form-group
.form-check
= f.check_box :invisible_captcha_enabled, class: 'form-check-input'
@@ -36,12 +47,18 @@
%span.form-text.text-muted
= _('Helps prevent bots from creating accounts.')
+ %h5
+ = _('Akismet')
+ %p
+ = _('Akismet helps prevent the creation of spam issues in public projects.')
+ = link_to _('How do I configure Akismet?'), help_page_path('integration/akismet.md'), target: '_blank', rel: 'noopener noreferrer'
+
.form-group
.form-check
= f.check_box :akismet_enabled, class: 'form-check-input'
= f.label :akismet_enabled, class: 'form-check-label' do
Enable Akismet
- %span.form-text.text-muted#akismet_help_block= _("Helps prevent bots from creating issues")
+ %span.form-text.text-muted#akismet_help_block= _("Helps prevent bots from creating issues.")
.form-group
= f.label :akismet_api_key, _('Akismet API Key'), class: 'label-bold'
@@ -50,25 +67,31 @@
Generate API key at
%a{ href: 'http://www.akismet.com', target: 'blank' } http://www.akismet.com
+ %h5
+ = _('IP address restrictions')
+
.form-group
.form-check
= f.check_box :unique_ips_limit_enabled, class: 'form-check-input'
= f.label :unique_ips_limit_enabled, class: 'form-check-label' do
- = _("Limit sign in from multiple ips")
+ = _("Limit sign in from multiple IP addresses")
%span.form-text.text-muted#unique_ip_help_block
- = _("Helps prevent malicious users hide their activity")
+ = _("Helps prevent malicious users hide their activity.")
.form-group
- = f.label :unique_ips_limit_per_user, _('IPs per user'), class: 'label-bold'
+ = f.label :unique_ips_limit_per_user, _('IP addresses per user'), class: 'label-bold'
= f.number_field :unique_ips_limit_per_user, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("Maximum number of unique IPs per user")
+ = _("Maximum number of unique IP addresses per user.")
.form-group
- = f.label :unique_ips_limit_time_window, _('IP expiration time'), class: 'label-bold'
+ = f.label :unique_ips_limit_time_window, _('IP address expiration time'), class: 'label-bold'
= f.number_field :unique_ips_limit_time_window, class: 'form-control gl-form-input'
.form-text.text-muted
- = _("How many seconds an IP will be counted towards the limit")
+ = _("How many seconds an IP counts toward the IP address limit.")
+
+ %h5
+ = _('Spam Check')
.form-group
.form-check
@@ -79,8 +102,8 @@
= f.label :spam_check_endpoint_url, _('URL of the external Spam Check endpoint'), class: 'label-bold'
= f.text_field :spam_check_endpoint_url, class: 'form-control gl-form-input'
.form-group
- = f.label :spam_check_api_key, _('Spam Check API Key'), class: 'gl-font-weight-bold'
+ = f.label :spam_check_api_key, _('Spam Check API key'), class: 'gl-font-weight-bold'
= f.text_field :spam_check_api_key, class: 'form-control gl-form-input'
- .form-text.text-muted= _('The API key used by GitLab for accessing the Spam Check service endpoint')
+ .form-text.text-muted= _('The API key used by GitLab for accessing the Spam Check service endpoint.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_terminal.html.haml b/app/views/admin/application_settings/_terminal.html.haml
index d6e31a24cf..c53f63e124 100644
--- a/app/views/admin/application_settings/_terminal.html.haml
+++ b/app/views/admin/application_settings/_terminal.html.haml
@@ -6,5 +6,5 @@
= f.label :terminal_max_session_time, _('Max session time'), class: 'label-bold'
= f.number_field :terminal_max_session_time, class: 'form-control gl-form-input'
.form-text.text-muted
- = _('Maximum time for web terminal websocket connection (in seconds). 0 for unlimited.')
+ = _('Maximum time, in seconds, for a web terminal websocket connection. 0 for unlimited.')
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/_usage.html.haml b/app/views/admin/application_settings/_usage.html.haml
index ddd0abb4c3..5bdad50c16 100644
--- a/app/views/admin/application_settings/_usage.html.haml
+++ b/app/views/admin/application_settings/_usage.html.haml
@@ -10,21 +10,21 @@
= f.label :version_check_enabled, class: 'form-check-label' do
= _("Enable version check")
.form-text.text-muted
- = _("GitLab will inform you if a new version is available.")
- = _("%{link_start}Learn more%{link_end} about what information is shared with GitLab Inc.").html_safe % { link_start: "".html_safe, link_end: ''.html_safe }
+ = _("GitLab informs you if a new version is available.")
+ = _("%{link_start}What information does GitLab Inc. collect?%{link_end}").html_safe % { link_start: "".html_safe, link_end: ''.html_safe }
.form-group
- can_be_configured = @application_setting.usage_ping_can_be_configured?
.form-check
= f.check_box :usage_ping_enabled, disabled: !can_be_configured, class: 'form-check-input'
= f.label :usage_ping_enabled, class: 'form-check-label' do
- = _('Enable service ping')
+ = _('Enable Service Ping')
.form-text.text-muted
- if can_be_configured
- %p.mb-2= _('To help improve GitLab and its user experience, GitLab will periodically collect usage information.')
+ %p.mb-2= _('To help improve GitLab and its user experience, GitLab periodically collects usage information.')
- - service_ping_path = help_page_path('user/admin_area/settings/usage_statistics', anchor: 'service-ping')
+ - service_ping_path = help_page_path('development/service_ping/index.md')
- service_ping_link_start = ''.html_safe % { url: service_ping_path }
- %p.mb-2= s_('%{service_ping_link_start}Learn more%{service_ping_link_end} about what information is shared with GitLab Inc.').html_safe % { service_ping_link_start: service_ping_link_start, service_ping_link_end: ''.html_safe }
+ %p.mb-2= s_('%{service_ping_link_start}What information is shared with GitLab Inc.?%{service_ping_link_end}').html_safe % { service_ping_link_start: service_ping_link_start, service_ping_link_end: ''.html_safe }
%button.gl-button.btn.btn-default.js-payload-preview-trigger{ type: 'button', data: { payload_selector: ".#{payload_class}" } }
.gl-spinner.js-spinner.gl-display-none.gl-mr-2
@@ -46,15 +46,23 @@
- if usage_ping_enabled
%p.gl-mb-3.text-muted{ id: 'service_ping_features_helper_text' }= _('You can enable Registration Features because Service Ping is enabled. To continue using Registration Features in the future, you will also need to register with GitLab via a new cloud licensing service.')
- else
- %p.gl-mb-3.text-muted{ id: 'service_ping_features_helper_text' }= _('To enable Registration Features, make sure "Enable service ping" is checked.')
+ %p.gl-mb-3.text-muted{ id: 'service_ping_features_helper_text' }= _('To enable Registration Features, first enable Service Ping.')
%p.gl-mb-3.text-muted= _('Registration Features include:')
.form-text
- email_from_gitlab_path = help_page_path('tools/email.md')
+ - repo_size_limit_path = help_page_path('user/admin_area/settings/account_and_limit_settings.md', anchor: 'repository-size-limit')
+ - restrict_ip_path = help_page_path('user/group/index.md', anchor: 'restrict-group-access-by-ip-address')
- link_end = ''.html_safe
- email_from_gitlab_link = ''.html_safe % { url: email_from_gitlab_path }
+ - repo_size_limit_link = ''.html_safe % { url: repo_size_limit_path }
+ - restrict_ip_link = ''.html_safe % { url: restrict_ip_path }
%ul
%li
= _('Email from GitLab - email users right from the Admin Area. %{link_start}Learn more%{link_end}.').html_safe % { link_start: email_from_gitlab_link, link_end: link_end }
+ %li
+ = _('Limit project size at a global, group, and project level. %{link_start}Learn more%{link_end}.').html_safe % { link_start: repo_size_limit_link, link_end: link_end }
+ %li
+ = _('Restrict group access by IP address. %{link_start}Learn more%{link_end}.').html_safe % { link_start: restrict_ip_link, link_end: link_end }
= f.submit _('Save changes'), class: "gl-button btn btn-confirm"
diff --git a/app/views/admin/application_settings/appearances/preview_sign_in.html.haml b/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
index 77c37abbee..2e4ab71404 100644
--- a/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
+++ b/app/views/admin/application_settings/appearances/preview_sign_in.html.haml
@@ -1,12 +1,13 @@
= render 'devise/shared/tab_single', tab_title: _('Sign in preview')
.login-box
%form.gl-show-field-errors
+ - title = _('This form is disabled in preview')
.form-group
= label_tag :login
- = text_field_tag :login, nil, class: "form-control gl-form-input top", title: _('Please provide your username or email address.')
+ = text_field_tag :login, nil, disabled: true, class: "form-control gl-form-input top", title: title
.form-group
= label_tag :password
- = password_field_tag :password, nil, class: "form-control gl-form-input bottom", title: _('This field is required.')
+ = password_field_tag :password, nil, disabled: true, class: "form-control gl-form-input bottom", title: title
.form-group
- = button_tag _("Sign in"), class: "btn gl-button btn-confirm", type: "button"
+ = button_tag _("Sign in"), disabled: true, class: "btn gl-button btn-confirm", type: "button", title: title
diff --git a/app/views/admin/application_settings/general.html.haml b/app/views/admin/application_settings/general.html.haml
index 9102769cc6..a72c96bb57 100644
--- a/app/views/admin/application_settings/general.html.haml
+++ b/app/views/admin/application_settings/general.html.haml
@@ -79,7 +79,8 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Set max session time for web terminal.')
+ = _('Set the maximum session time for a web terminal.')
+ = link_to _('How do I use a web terminal?'), help_page_path('ci/environments/index.md', anchor: 'web-terminals'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'terminal'
diff --git a/app/views/admin/application_settings/metrics_and_profiling.html.haml b/app/views/admin/application_settings/metrics_and_profiling.html.haml
index f1e37c7613..6087551d7c 100644
--- a/app/views/admin/application_settings/metrics_and_profiling.html.haml
+++ b/app/views/admin/application_settings/metrics_and_profiling.html.haml
@@ -49,7 +49,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Enable or disable version check and service ping.')
+ = _('Enable or disable version check and Service Ping.')
.settings-content
= render 'usage'
diff --git a/app/views/admin/application_settings/network.html.haml b/app/views/admin/application_settings/network.html.haml
index 8dff2bc36c..58e3f3f113 100644
--- a/app/views/admin/application_settings/network.html.haml
+++ b/app/views/admin/application_settings/network.html.haml
@@ -35,9 +35,10 @@
= _('Set rate limits for package registry API requests that supersede the general user and IP rate limits.')
= link_to _('Learn more.'), help_page_path('user/admin_area/settings/package_registry_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
- = render 'package_registry_limits'
+ = render partial: 'network_rate_limits', locals: { anchor: 'js-packages-limits-settings', setting_fragment: 'packages_api' }
+
- if Feature.enabled?(:files_api_throttling, default_enabled: :yaml)
- %section.settings.as-files-limits.no-animate#js-files-limits-settings{ class: ('expanded' if expanded_by_default?), data: { testid: 'files-limits-settings' } }
+ %section.settings.as-files-limits.no-animate#js-files-limits-settings{ class: ('expanded' if expanded_by_default?) }
.settings-header
%h4
= _('Files API Rate Limits')
@@ -46,7 +47,19 @@
%p
= _('Configure specific limits for Files API requests that supersede the general user and IP rate limits.')
.settings-content
- = render 'files_limits'
+ = render partial: 'network_rate_limits', locals: { anchor: 'js-files-limits-settings', setting_fragment: 'files_api' }
+
+%section.settings.as-deprecated-limits.no-animate#js-deprecated-limits-settings{ class: ('expanded' if expanded_by_default?) }
+ .settings-header
+ %h4
+ = _('Deprecated API rate limits')
+ %button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
+ = expanded_by_default? ? _('Collapse') : _('Expand')
+ %p
+ = _('Configure specific limits for deprecated API requests that supersede the general user and IP rate limits.')
+ = link_to _('Which API requests are affected?'), help_page_path('user/admin_area/settings/deprecated_api_rate_limits.md'), target: '_blank', rel: 'noopener noreferrer'
+ .settings-content
+ = render partial: 'network_rate_limits', locals: { anchor: 'js-deprecated-limits-settings', setting_fragment: 'deprecated_api' }
%section.settings.as-git-lfs-limits.no-animate#js-git-lfs-limits-settings{ class: ('expanded' if expanded_by_default?), data: { qa_selector: 'git_lfs_limits_content' } }
.settings-header
diff --git a/app/views/admin/application_settings/reporting.html.haml b/app/views/admin/application_settings/reporting.html.haml
index 914a09ff5d..d2e118f062 100644
--- a/app/views/admin/application_settings/reporting.html.haml
+++ b/app/views/admin/application_settings/reporting.html.haml
@@ -9,9 +9,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- - recaptcha_v2_link_url = 'https://developers.google.com/recaptcha/docs/versions'
- - recaptcha_v2_link_start = ''.html_safe % { url: recaptcha_v2_link_url }
- = _('Enable reCAPTCHA, Invisible Captcha, Akismet and set IP limits. For reCAPTCHA, we currently only support %{recaptcha_v2_link_start}v2%{recaptcha_v2_link_end}').html_safe % { recaptcha_v2_link_start: recaptcha_v2_link_start, recaptcha_v2_link_end: ''.html_safe }
+ = _('Configure CAPTCHAs, IP address limits, and other anti-spam measures.')
.settings-content
= render 'spam'
@@ -22,6 +20,7 @@
%button.btn.gl-button.btn-default.js-settings-toggle{ type: 'button' }
= expanded_by_default? ? _('Collapse') : _('Expand')
%p
- = _('Set notification email for abuse reports.')
+ = _('Receive notification of abuse reports by email.')
+ = link_to _('Learn more.'), help_page_path('user/admin_area/review_abuse_reports.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
= render 'abuse'
diff --git a/app/views/admin/dashboard/_security_newsletter_callout.html.haml b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
new file mode 100644
index 0000000000..ece0f7ca4d
--- /dev/null
+++ b/app/views/admin/dashboard/_security_newsletter_callout.html.haml
@@ -0,0 +1,14 @@
+- return unless show_security_newsletter_user_callout?
+
+= render 'shared/global_alert',
+ title: s_('AdminArea|Get security updates from GitLab and stay up to date'),
+ variant: :tip,
+ alert_class: 'js-security-newsletter-callout',
+ is_contained: true,
+ alert_data: { feature_id: UserCalloutsHelper::SECURITY_NEWSLETTER_CALLOUT, dismiss_endpoint: user_callouts_path, defer_links: 'true' },
+ close_button_data: { testid: 'close-security-newsletter-callout' } do
+ .gl-alert-body
+ = s_('AdminArea|Sign up for the GitLab Security Newsletter to get notified for security updates.')
+ .gl-alert-actions
+ = link_to 'https://about.gitlab.com/company/preference-center/', target: '_blank', rel: 'noreferrer noopener', class: 'deferred-link gl-alert-action btn-confirm btn-md gl-button' do
+ = s_('AdminArea|Sign up for the GitLab newsletter')
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 97b3a757a3..681e7ccb61 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -4,6 +4,7 @@
- billable_users_link_start = ''.html_safe % { url: billable_users_url }
= render_if_exists 'shared/qrtly_reconciliation_alert'
+= render 'admin/dashboard/security_newsletter_callout'
- if @notices
- @notices.each do |notice|
diff --git a/app/views/admin/hook_logs/_index.html.haml b/app/views/admin/hook_logs/_index.html.haml
index a7f947f96e..6a46b0b351 100644
--- a/app/views/admin/hook_logs/_index.html.haml
+++ b/app/views/admin/hook_logs/_index.html.haml
@@ -1,37 +1,11 @@
+- docs_link_url = help_page_path('user/project/integrations/webhooks', anchor: 'troubleshoot-webhooks')
+- link_start = ''.html_safe % { url: docs_link_url }
+- link_end = ''.html_safe
+
.row.gl-mt-3.gl-mb-3
.col-lg-3
%h4.gl-mt-0
- = _('Recent Deliveries')
- %p= _('When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.')
+ = _('Recent events')
+ %p= _('GitLab events trigger webhooks. Use the request details of a webhook to help troubleshoot problems. %{link_start}How do I troubleshoot?%{link_end}').html_safe % { link_start: link_start, link_end: link_end }
.col-lg-9
- - if hook_logs.present?
- %table.table
- %thead
- %tr
- %th= _('Status')
- %th= _('Trigger')
- %th= _('URL')
- %th= _('Elapsed time')
- %th= _('Request time')
- %th
- - hook_logs.each do |hook_log|
- %tr
- %td
- = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.d-none.d-sm-block
- %span.badge.badge-gray.deploy-project-label
- = hook_log.trigger.singularize.titleize
- %td
- = truncate(hook_log.url, length: 50)
- %td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
- %td.light
- = time_ago_with_tooltip(hook_log.created_at)
- %td
- = link_to _('View details'), admin_hook_hook_log_path(hook, hook_log)
-
- = paginate hook_logs, theme: 'gitlab'
-
- - else
- .settings-message.text-center
- = _("You don't have any webhooks deliveries")
+ = render partial: 'shared/hook_logs/recent_deliveries_table', locals: { hook: hook, hook_logs: hook_logs }
diff --git a/app/views/admin/projects/index.html.haml b/app/views/admin/projects/index.html.haml
index 5ebfd296e2..f947e17499 100644
--- a/app/views/admin/projects/index.html.haml
+++ b/app/views/admin/projects/index.html.haml
@@ -1,33 +1,24 @@
- page_title _('Projects')
- params[:visibility_level] ||= []
-- active_tab_classes = 'active gl-tab-nav-item-active gl-tab-nav-item-active-indigo'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
- %ul.nav.gl-tabs-nav.gl-overflow-x-auto.gl-display-flex.gl-flex-grow-1.gl-flex-shrink-1.gl-border-b-0.gl-flex-nowrap.gl-webkit-scrollbar-display-none
- = nav_link(html_options: { class: "nav-item" } ) do
- = link_to _('All'), admin_projects_path, class: "nav-link gl-tab-nav-item #{active_tab_classes if params[:visibility_level].empty?}"
- = nav_link(html_options: { class: "nav-item" } ) do
- = link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE), class: "nav-link gl-tab-nav-item #{active_tab_classes if params[:visibility_level] == Gitlab::VisibilityLevel::PRIVATE.to_s}"
- = nav_link(html_options: { class: "nav-item" } ) do
- = link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL), class: "nav-link gl-tab-nav-item #{active_tab_classes if params[:visibility_level] == Gitlab::VisibilityLevel::INTERNAL.to_s}"
- = nav_link(html_options: { class: "nav-item" } ) do
- = link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC), class: "nav-link gl-tab-nav-item #{active_tab_classes if params[:visibility_level] == Gitlab::VisibilityLevel::PUBLIC.to_s}"
+ = gl_tabs_nav({ class: 'gl-border-b-0 gl-overflow-x-auto gl-flex-grow-1 gl-flex-nowrap gl-webkit-scrollbar-display-none' }) do
+ = gl_tab_link_to _('All'), admin_projects_path(visibility_level: nil), { item_active: params[:visibility_level].empty? }
+ = gl_tab_link_to _('Private'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PRIVATE)
+ = gl_tab_link_to _('Internal'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::INTERNAL)
+ = gl_tab_link_to _('Public'), admin_projects_path(visibility_level: Gitlab::VisibilityLevel::PUBLIC)
.nav-controls
.search-holder
= render 'shared/projects/search_form', autofocus: true, admin_view: true
- .dropdown
- - toggle_text = _('Namespace')
- - if params[:namespace_id].present?
- = hidden_field_tag :namespace_id, params[:namespace_id]
- - namespace = Namespace.find(params[:namespace_id])
- - toggle_text = "#{namespace.kind}: #{namespace.full_path}"
- = dropdown_toggle(toggle_text, { toggle: 'dropdown', is_filter: 'true' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select.dropdown-menu-right
- = dropdown_title(_('Namespaces'))
- = dropdown_filter(_("Search for Namespace"))
- = dropdown_content
- = dropdown_loading
+ - current_namespace = _('Namespace')
+ - if params[:namespace_id].present?
+ - namespace = Namespace.find(params[:namespace_id])
+ - current_namespace = "#{namespace.kind}: #{namespace.full_path}"
+ %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { show_any: 'true', field_name: 'namespace_id', placeholder: current_namespace, update_location: 'true' }, type: 'button' }
+ %span.gl-new-dropdown-button-text
+ = current_namespace
+
= render 'shared/projects/dropdown'
= link_to new_project_path, class: 'gl-button btn btn-confirm' do
= _('New Project')
diff --git a/app/views/admin/projects/show.html.haml b/app/views/admin/projects/show.html.haml
index 1a87b21351..3069aab271 100644
--- a/app/views/admin/projects/show.html.haml
+++ b/app/views/admin/projects/show.html.haml
@@ -143,13 +143,10 @@
.col-sm-3.col-form-label
= f.label :new_namespace_id, _("Namespace")
.col-sm-9
- .dropdown
- = dropdown_toggle(_('Search for Namespace'), { toggle: 'dropdown', field_name: 'new_namespace_id' }, { toggle_class: 'js-namespace-select large' })
- .dropdown-menu.dropdown-select
- = dropdown_title(_('Namespaces'))
- = dropdown_filter(_('Search for Namespace'))
- = dropdown_content
- = dropdown_loading
+ - placeholder = _('Search for Namespace')
+ %button.dropdown-menu-toggle.btn.btn-default.btn-md.gl-button.js-namespace-select{ data: { field_name: 'new_namespace_id', placeholder: placeholder }, type: 'button' }
+ %span.gl-new-dropdown-button-text
+ = placeholder
.form-group.row
.offset-sm-3.col-sm-9
diff --git a/app/views/admin/runners/show.html.haml b/app/views/admin/runners/show.html.haml
index 59523ed3a0..808b2bb4f8 100644
--- a/app/views/admin/runners/show.html.haml
+++ b/app/views/admin/runners/show.html.haml
@@ -9,7 +9,7 @@
.row
.col-md-6
%h4= _('Restrict projects for this runner')
- - if @runner.projects.any?
+ - if @runner.runner_projects.any?
%table.table{ data: { testid: 'assigned-projects' } }
%thead
%tr
diff --git a/app/views/admin/serverless/domains/_form.html.haml b/app/views/admin/serverless/domains/_form.html.haml
deleted file mode 100644
index a3e1ccc5d4..0000000000
--- a/app/views/admin/serverless/domains/_form.html.haml
+++ /dev/null
@@ -1,99 +0,0 @@
-- form_name = 'js-serverless-domain-settings'
-- form_url = @domain.persisted? ? admin_serverless_domain_path(@domain.id, anchor: form_name) : admin_serverless_domains_path(anchor: form_name)
-- show_certificate_card = @domain.persisted? && @domain.errors.blank?
-= form_for @domain, url: form_url, html: { class: 'fieldset-form' } do |f|
- = form_errors(@domain)
-
- %fieldset
- - if @domain.persisted?
- - dns_record = "*.#{@domain.domain} CNAME #{Settings.pages.host}."
- - verification_record = "#{@domain.verification_domain} TXT #{@domain.keyed_verification_code}"
- .form-group.row
- .col-sm-6.position-relative
- = f.label :domain, _('Domain'), class: 'label-bold'
- = f.text_field :domain, class: 'form-control has-floating-status-badge', readonly: true
- .status-badge.floating-status-badge
- - text, status = @domain.unverified? ? [_('Unverified'), 'badge-danger'] : [_('Verified'), 'badge-success']
- .badge{ class: status }
- = text
- = link_to sprite_icon("redo"), verify_admin_serverless_domain_path(@domain.id), method: :post, class: "gl-button btn has-tooltip", title: _("Retry verification")
-
- .col-sm-6
- = f.label :serverless_domain_dns, _('DNS'), class: 'label-bold'
- .input-group
- = text_field_tag :serverless_domain_dns, dns_record , class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#serverless_domain_dns', class: 'btn-default input-group-text d-none d-sm-block')
-
- .col-sm-12.form-text.text-muted
- = _("To access this domain create a new DNS record")
-
- .form-group
- = f.label :serverless_domain_verification, _('Verification status'), class: 'label-bold'
- .input-group
- = text_field_tag :serverless_domain_verification, verification_record, class: "monospace js-select-on-focus form-control", readonly: true
- .input-group-append
- = clipboard_button(target: '#serverless_domain_verification', class: 'btn-default d-none d-sm-block')
- %p.form-text.text-muted
- - link_to_help = link_to(_('verify ownership'), help_page_path('user/project/pages/custom_domains_ssl_tls_certification/index.md', anchor: '4-verify-the-domains-ownership'))
- = _("To %{link_to_help} of your domain, add the above key to a TXT record within your DNS configuration.").html_safe % { link_to_help: link_to_help }
-
- - else
- .form-group
- = f.label :domain, _('Domain'), class: 'label-bold'
- = f.text_field :domain, class: 'form-control'
-
- - if show_certificate_card
- .card.js-domain-cert-show
- .card-header
- = _('Certificate')
- .d-flex.justify-content-between.align-items-center.p-3
- %span
- = @domain.subject || _('missing')
- %button.gl-button.btn.btn-danger.btn-sm.js-domain-cert-replace-btn{ type: 'button' }
- = _('Replace')
-
- .js-domain-cert-inputs{ class: ('hidden' if show_certificate_card) }
- .form-group
- = f.label :user_provided_certificate, _('Certificate (PEM)'), class: 'label-bold'
- = f.text_area :user_provided_certificate, rows: 5, class: 'form-control', value: ''
- %span.form-text.text-muted
- = _("Upload a certificate for your domain with all intermediates")
- .form-group
- = f.label :user_provided_key, _('Key (PEM)'), class: 'label-bold'
- = f.text_area :user_provided_key, rows: 5, class: 'form-control', value: ''
- %span.form-text.text-muted
- = _("Upload a private key for your certificate")
-
- = f.submit @domain.persisted? ? _('Save changes') : _('Add domain'), class: "gl-button btn btn-confirm js-serverless-domain-submit", disabled: @domain.persisted?
- - if @domain.persisted?
- %button.gl-button.btn.btn-danger{ type: 'button', data: { toggle: 'modal', target: "#modal-delete-domain" } }
- = _('Delete domain')
-
--# haml-lint:disable NoPlainNodes
-- if @domain.persisted?
- - domain_attached = @domain.serverless_domain_clusters.count > 0
- .modal{ id: "modal-delete-domain", tabindex: -1 }
- .modal-dialog
- .modal-content
- .modal-header
- %h3.page-title= _('Delete serverless domain?')
- %button.close{ type: "button", "data-dismiss": "modal", "aria-label" => _('Close') }
- %span{ "aria-hidden": "true" } ×
-
- .modal-body
- - if domain_attached
- = _("You must disassociate %{domain} from all clusters it is attached to before deletion.").html_safe % { domain: "#{@domain.domain}
".html_safe }
- - else
- = _("You are about to delete %{domain} from your instance. This domain will no longer be available to any Knative application.").html_safe % { domain: "#{@domain.domain}
".html_safe }
-
- .modal-footer
- %a{ href: '#', data: { dismiss: 'modal' }, class: 'gl-button btn btn-default' }
- = _('Cancel')
-
- = link_to _('Delete domain'),
- admin_serverless_domain_path(@domain.id),
- title: _('Delete'),
- method: :delete,
- class: "gl-button btn btn-danger",
- disabled: domain_attached
diff --git a/app/views/admin/serverless/domains/index.html.haml b/app/views/admin/serverless/domains/index.html.haml
deleted file mode 100644
index c2b6baed4d..0000000000
--- a/app/views/admin/serverless/domains/index.html.haml
+++ /dev/null
@@ -1,25 +0,0 @@
-- breadcrumb_title _("Operations")
-- page_title _("Operations")
-- @content_class = "limit-container-width" unless fluid_layout
-
--# normally expanded_by_default? is used here, but since this is the only panel
--# in this settings page, let's leave it always open by default
-- expanded = true
-
-%section.settings.as-serverless-domain.no-animate#js-serverless-domain-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4
- = _('Serverless domain')
- %button.gl-button.btn.btn-default.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p
- = _('Set an instance-wide domain that will be available to all clusters when installing Knative.')
- .settings-content
- - if Gitlab.config.pages.enabled
- = render 'form'
- - else
- .card
- .card-header
- = s_('GitLabPages|Domains')
- .nothing-here-block
- = s_("GitLabPages|Support for domains and certificates is disabled. Ask your system's administrator to enable it.")
diff --git a/app/views/admin/sessions/_new_base.html.haml b/app/views/admin/sessions/_new_base.html.haml
index 47ef4f2688..c9b002a4dd 100644
--- a/app/views/admin/sessions/_new_base.html.haml
+++ b/app/views/admin/sessions/_new_base.html.haml
@@ -1,7 +1,7 @@
= form_tag(admin_session_path, method: :post, class: 'new_user gl-show-field-errors', 'aria-live': 'assertive') do
.form-group
= label_tag :user_password, _('Password'), class: 'label-bold'
- = password_field_tag 'user[password]', nil, class: 'form-control', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
+ = password_field_tag 'user[password]', nil, class: 'form-control', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
.submit-container.move-submit-down
= submit_tag _('Enter Admin Mode'), class: 'gl-button btn btn-success', data: { qa_selector: 'enter_admin_mode_button' }
diff --git a/app/views/admin/topics/_form.html.haml b/app/views/admin/topics/_form.html.haml
new file mode 100644
index 0000000000..21a1d74a8c
--- /dev/null
+++ b/app/views/admin/topics/_form.html.haml
@@ -0,0 +1,40 @@
+= gitlab_ui_form_for @topic, url: url, html: { multipart: true, class: 'js-project-topic-form gl-show-field-errors common-note-form js-quick-submit js-requires-input' }, authenticity_token: true do |f|
+ = form_errors(@topic)
+
+ .form-group
+ = f.label :name do
+ = _("Topic name")
+ = f.text_field :name, placeholder: _('My topic'), class: 'form-control input-lg', data: { qa_selector: 'topic_name_field' },
+ required: true,
+ title: _('Please fill in a name for your topic.'),
+ autofocus: true
+
+ .form-group
+ = f.label :description, _("Description")
+ = render layout: 'shared/md_preview', locals: { url: preview_markdown_admin_topics_path, referenced_users: false } do
+ = render 'shared/zen', f: f, attr: :description,
+ classes: 'note-textarea',
+ placeholder: _('Write a description…'),
+ supports_quick_actions: false,
+ supports_autocomplete: false,
+ qa_selector: 'topic_form_description'
+ = render 'shared/notes/hints', supports_file_upload: false
+
+ .form-group.gl-mt-3.gl-mb-3
+ = f.label :avatar, _('Topic avatar'), class: 'gl-display-block'
+ - if @topic.avatar?
+ .avatar-container.rect-avatar.s90
+ = topic_icon(@topic, alt: _('Topic avatar'), class: 'avatar topic-avatar s90')
+ = render 'shared/choose_avatar_button', f: f
+ - if @topic.avatar?
+ = link_to _('Remove avatar'), admin_topic_avatar_path(@topic), data: { confirm: _('Avatar will be removed. Are you sure?')}, method: :delete, class: 'gl-button btn btn-danger-secondary gl-mt-2'
+
+ - if @topic.new_record?
+ .form-actions
+ = f.submit _('Create topic'), class: "gl-button btn btn-confirm"
+ = link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-default btn-cancel"
+
+ - else
+ .form-actions
+ = f.submit _('Save changes'), class: "gl-button btn btn-confirm", data: { qa_selector: 'save_changes_button' }
+ = link_to _('Cancel'), admin_topics_path, class: "gl-button btn btn-cancel"
diff --git a/app/views/admin/topics/_topic.html.haml b/app/views/admin/topics/_topic.html.haml
new file mode 100644
index 0000000000..abf3cffa42
--- /dev/null
+++ b/app/views/admin/topics/_topic.html.haml
@@ -0,0 +1,17 @@
+- topic = local_assigns.fetch(:topic)
+
+%li.topic-row.gl-py-3.gl-align-items-center{ class: 'gl-display-flex!', data: { qa_selector: 'topic_row_content' } }
+ .avatar-container.rect-avatar.s40.gl-flex-shrink-0
+ = topic_icon(topic, class: "avatar s40")
+
+ .gl-min-w-0.gl-flex-grow-1
+ .title
+ = topic.name
+
+ .stats.gl-text-gray-500.gl-flex-shrink-0.gl-display-none.gl-sm-display-flex
+ %span.gl-ml-5.has-tooltip{ title: n_('%d project', '%d projects', topic.total_projects_count) % topic.total_projects_count }
+ = sprite_icon('bookmark', css_class: 'gl-vertical-align-text-bottom')
+ = number_with_delimiter(topic.total_projects_count)
+
+ .controls.gl-flex-shrink-0.gl-ml-5
+ = link_to _('Edit'), edit_admin_topic_path(topic), id: "edit_#{dom_id(topic)}", class: 'btn gl-button btn-default'
diff --git a/app/views/admin/topics/edit.html.haml b/app/views/admin/topics/edit.html.haml
new file mode 100644
index 0000000000..4416bb0fe1
--- /dev/null
+++ b/app/views/admin/topics/edit.html.haml
@@ -0,0 +1,4 @@
+- page_title _("Edit"), @topic.name, _("Topics")
+%h3.page-title= _('Edit topic: %{topic_name}') % { topic_name: @topic.name }
+%hr
+= render 'form', url: admin_topic_path(@topic)
diff --git a/app/views/admin/topics/index.html.haml b/app/views/admin/topics/index.html.haml
new file mode 100644
index 0000000000..6485b8aa41
--- /dev/null
+++ b/app/views/admin/topics/index.html.haml
@@ -0,0 +1,19 @@
+- page_title _("Topics")
+
+= form_tag admin_topics_path, method: :get do |f|
+ .gl-py-3.gl-display-flex.gl-flex-direction-column-reverse.gl-md-flex-direction-row.gl-border-b-solid.gl-border-gray-100.gl-border-b-1
+ .gl-flex-grow-1.gl-mt-3.gl-md-mt-0
+ .inline.gl-w-full.gl-md-w-auto
+ - search = params.fetch(:search, nil)
+ .search-field-holder
+ = search_field_tag :search, search, class: "form-control gl-form-input search-text-input js-search-input", autofocus: true, spellcheck: false, placeholder: _('Search by name'), data: { qa_selector: 'topic_search_field' }
+ = sprite_icon('search', css_class: 'search-icon')
+ .nav-controls
+ = link_to new_admin_topic_path, class: "gl-button btn btn-confirm gl-w-full gl-md-w-auto" do
+ = _('New topic')
+%ul.content-list
+ = render partial: 'topic', collection: @topics
+
+= paginate_collection @topics
+- if @topics.empty?
+ = render 'shared/empty_states/topics'
diff --git a/app/views/admin/topics/new.html.haml b/app/views/admin/topics/new.html.haml
new file mode 100644
index 0000000000..8b4a8ac269
--- /dev/null
+++ b/app/views/admin/topics/new.html.haml
@@ -0,0 +1,4 @@
+- page_title _("New topic")
+%h3.page-title= _('New topic')
+%hr
+= render 'form', url: admin_topics_path(@topic)
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index aeb274fe2c..6a5f07dd2d 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -19,22 +19,20 @@
.col-sm-10
- editing_current_user = (current_user == @user)
- = f.radio_button :access_level, :regular, disabled: editing_current_user
- = f.label :access_level_regular, class: 'font-weight-bold' do
- = s_('AdminUsers|Regular')
- %p.light
- = s_('AdminUsers|Regular users have access to their groups and projects')
+ = f.gitlab_ui_radio_component :access_level, :regular,
+ s_('AdminUsers|Regular'),
+ radio_options: { disabled: editing_current_user },
+ help_text: s_('AdminUsers|Regular users have access to their groups and projects.')
= render_if_exists 'admin/users/auditor_access_level_radio', f: f, disabled: editing_current_user
- = f.radio_button :access_level, :admin, disabled: editing_current_user
- = f.label :access_level_admin, class: 'font-weight-bold' do
- = s_('AdminUsers|Admin')
- %p.light
- = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation')
- - if editing_current_user
- %p.light
- = s_('AdminUsers|You cannot remove your own admin rights.')
+ - help_text = s_('AdminUsers|Administrators have access to all groups, projects and users and can manage all features in this installation.')
+ - help_text += ' ' + s_('AdminUsers|You cannot remove your own admin rights.') if editing_current_user
+ = f.gitlab_ui_radio_component :access_level, :admin,
+ s_('AdminUsers|Admin'),
+ radio_options: { disabled: editing_current_user },
+ help_text: help_text
+
.form-group.row
.col-sm-2.col-form-label.gl-pt-0
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 9d62c19e2f..3869a2b6dc 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -1,5 +1,5 @@
.user_new
- = form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
+ = gitlab_ui_form_for [:admin, @user], html: { class: 'fieldset-form' } do |f|
= form_errors(@user)
%fieldset
@@ -39,12 +39,12 @@
.col-sm-2.col-form-label
= f.label :password
.col-sm-10
- = f.password_field :password, disabled: f.object.force_random_password, class: 'form-control gl-form-input'
+ = f.password_field :password, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
.form-group.row
.col-sm-2.col-form-label
= f.label :password_confirmation
.col-sm-10
- = f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control gl-form-input'
+ = f.password_field :password_confirmation, disabled: f.object.force_random_password, autocomplete: 'new-password', class: 'form-control gl-form-input'
= render partial: 'access_levels', locals: { f: f }
diff --git a/app/views/admin/users/_head.html.haml b/app/views/admin/users/_head.html.haml
index f4b1a2853f..bafb208558 100644
--- a/app/views/admin/users/_head.html.haml
+++ b/app/views/admin/users/_head.html.haml
@@ -33,16 +33,11 @@
- if can_force_email_confirmation?(@user)
%button.btn.gl-button.btn-info.js-confirm-modal-button{ data: confirm_user_data(@user) }
= _('Confirm user')
-%ul.nav-links.nav.nav-tabs
- = nav_link(path: 'users#show') do
- = link_to _("Account"), admin_user_path(@user)
- = nav_link(path: 'users#projects') do
- = link_to _("Groups and projects"), projects_admin_user_path(@user)
- = nav_link(path: 'users#keys') do
- = link_to _("SSH keys"), keys_admin_user_path(@user)
- = nav_link(controller: :identities) do
- = link_to _("Identities"), admin_user_identities_path(@user)
+= gl_tabs_nav do
+ = gl_tab_link_to _("Account"), admin_user_path(@user)
+ = gl_tab_link_to _("Groups and projects"), projects_admin_user_path(@user)
+ = gl_tab_link_to _("SSH keys"), keys_admin_user_path(@user)
+ = gl_tab_link_to _("Identities"), admin_user_identities_path(@user)
- if impersonation_enabled?
- = nav_link(controller: :impersonation_tokens) do
- = link_to _("Impersonation Tokens"), admin_user_impersonation_tokens_path(@user)
+ = gl_tab_link_to _("Impersonation Tokens"), admin_user_impersonation_tokens_path(@user)
.gl-mb-3
diff --git a/app/views/admin/users/show.html.haml b/app/views/admin/users/show.html.haml
index ad8d9d1f04..2a9b4694e7 100644
--- a/app/views/admin/users/show.html.haml
+++ b/app/views/admin/users/show.html.haml
@@ -61,7 +61,6 @@
= _('Disabled')
= render_if_exists 'admin/namespace_plan_info', namespace: @user.namespace
- = render_if_exists 'admin/users/credit_card_info', user: @user
%li
%span.light= _('External User:')
@@ -139,6 +138,8 @@
= render_if_exists 'namespaces/shared_runner_status', namespace: @user.namespace
+ = render_if_exists 'admin/users/credit_card_info', user: @user, link_to_match_page: true
+
= render 'shared/custom_attributes', custom_attributes: @user.custom_attributes
-# Rendered on desktop only so order of cards can be different on desktop vs mobile
diff --git a/app/views/authentication/_authenticate.html.haml b/app/views/authentication/_authenticate.html.haml
index 5a2ae3f44c..7dcec50573 100644
--- a/app/views/authentication/_authenticate.html.haml
+++ b/app/views/authentication/_authenticate.html.haml
@@ -1,14 +1,17 @@
#js-authenticate-token-2fa
%a.gl-button.btn.btn-block.btn-confirm#js-login-2fa-device{ href: '#' }= _("Sign in via 2FA code")
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-token-2fa-in-progress{ type: "text/template" }
%p= _("Trying to communicate with your device. Plug it in (if you haven't already) and press the button on the device now.")
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-token-2fa-error{ type: "text/template" }
%div
%p <%= error_message %> (<%= error_name %>)
%a.btn.btn-default.gl-button.btn-block#js-token-2fa-try-again= _("Try again?")
+-# haml-lint:disable InlineJavaScript
%script#js-authenticate-token-2fa-authenticated{ type: "text/template" }
%div
%p= _("We heard back from your device. You have been authenticated.")
diff --git a/app/views/authentication/_register.html.haml b/app/views/authentication/_register.html.haml
index 678fd3c8e8..5eed969ed3 100644
--- a/app/views/authentication/_register.html.haml
+++ b/app/views/authentication/_register.html.haml
@@ -1,8 +1,10 @@
#js-register-token-2fa
+-# haml-lint:disable InlineJavaScript
%script#js-register-2fa-message{ type: "text/template" }
%p <%= message %>
+-# haml-lint:disable InlineJavaScript
%script#js-register-token-2fa-setup{ type: "text/template" }
- if current_user.two_factor_otp_enabled?
.row.gl-mb-3
@@ -17,12 +19,14 @@
.col-md-8
%p= _("You need to register a two-factor authentication app before you can set up a device.")
+-# haml-lint:disable InlineJavaScript
%script#js-register-token-2fa-error{ type: "text/template" }
%div
%p
%span <%= error_message %> (<%= error_name %>)
%a.btn.btn-default.gl-button#js-token-2fa-try-again= _("Try again?")
+-# haml-lint:disable InlineJavaScript
%script#js-register-token-2fa-registered{ type: "text/template" }
.row.gl-mb-3
.col-md-12
diff --git a/app/views/clusters/clusters/_advanced_settings_tab.html.haml b/app/views/clusters/clusters/_advanced_settings_tab.html.haml
index b491a64e43..8c9d98604d 100644
--- a/app/views/clusters/clusters/_advanced_settings_tab.html.haml
+++ b/app/views/clusters/clusters/_advanced_settings_tab.html.haml
@@ -1,6 +1,5 @@
- active = params[:tab] == 'settings'
- if can_admin_cluster?(current_user, @cluster)
- %li.nav-item{ role: 'presentation' }
- %a#cluster-settings-tab.nav-link{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'settings'}) }
- %span= _('Advanced Settings')
+ = gl_tab_link_to clusterable.cluster_path(@cluster.id, params: { tab: 'settings' }), { item_active: active } do
+ = _('Advanced Settings')
diff --git a/app/views/clusters/clusters/_details_tab.html.haml b/app/views/clusters/clusters/_details_tab.html.haml
index 564c5103d3..734910686e 100644
--- a/app/views/clusters/clusters/_details_tab.html.haml
+++ b/app/views/clusters/clusters/_details_tab.html.haml
@@ -1,5 +1,4 @@
- active = params[:tab] == 'details' || !params[:tab].present?
-%li.nav-item{ role: 'presentation' }
- %a#cluster-details-tab.nav-link.qa-details{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'details'}) }
- %span= _('Details')
+= gl_tab_link_to clusterable.cluster_path(@cluster.id, params: { tab: 'details' }), { item_active: active } do
+ = _('Details')
diff --git a/app/views/clusters/clusters/_health_tab.html.haml b/app/views/clusters/clusters/_health_tab.html.haml
index fda392693f..4292066cc6 100644
--- a/app/views/clusters/clusters/_health_tab.html.haml
+++ b/app/views/clusters/clusters/_health_tab.html.haml
@@ -1,5 +1,4 @@
- active = params[:tab] == 'health'
-%li.nav-item{ role: 'presentation' }
- %a#cluster-health-tab.nav-link.qa-health{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: 'health'}) }
- %span= _('Health')
+= gl_tab_link_to clusterable.cluster_path(@cluster.id, params: { tab: 'health' }), { item_active: active, data: { testid: 'cluster-health-tab' } } do
+ = _('Health')
diff --git a/app/views/clusters/clusters/_integrations_tab.html.haml b/app/views/clusters/clusters/_integrations_tab.html.haml
index 77b8b6ca3e..e229c1fbe1 100644
--- a/app/views/clusters/clusters/_integrations_tab.html.haml
+++ b/app/views/clusters/clusters/_integrations_tab.html.haml
@@ -1,6 +1,4 @@
-- tab_name = 'integrations'
-- active = params[:tab] == tab_name
+- active = params[:tab] == 'integrations'
-%li.nav-item{ role: 'presentation' }
- %a#cluster-apps-tab.nav-link{ class: active_when(active), href: clusterable.cluster_path(@cluster.id, params: {tab: tab_name}) }
- %span= _('Integrations')
+= gl_tab_link_to clusterable.cluster_path(@cluster.id, params: { tab: 'integrations' }), { item_active: active } do
+ = _('Integrations')
diff --git a/app/views/clusters/clusters/_namespace.html.haml b/app/views/clusters/clusters/_namespace.html.haml
index 9b728e7a89..6412972e19 100644
--- a/app/views/clusters/clusters/_namespace.html.haml
+++ b/app/views/clusters/clusters/_namespace.html.haml
@@ -1,7 +1,6 @@
- managed_namespace_help_text = s_('ClusterIntegration|Set a prefix for your namespaces. If not set, defaults to your project path. If modified, existing environments will use their current namespaces until the cluster cache is cleared.')
- non_managed_namespace_help_text = s_('ClusterIntegration|The namespace associated with your project. This will be used for deploy boards, logs, and Web terminals.')
-- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/index.md',
- anchor: 'gitlab-managed-clusters'), target: '_blank'
+- managed_namespace_help_link = link_to _('More information'), help_page_path('user/project/clusters/gitlab_managed_clusters.md'), target: '_blank'
.js-namespace-prefixed
= platform_field.text_field :namespace,
diff --git a/app/views/clusters/clusters/aws/_new.html.haml b/app/views/clusters/clusters/aws/_new.html.haml
index 93db7db06b..f6d50410e9 100644
--- a/app/views/clusters/clusters/aws/_new.html.haml
+++ b/app/views/clusters/clusters/aws/_new.html.haml
@@ -12,6 +12,6 @@
'role-arn' => @aws_role.role_arn,
'instance-types' => @instance_types,
'kubernetes-integration-help-path' => help_page_path('user/project/clusters/index'),
- 'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'create-a-new-certificate-based-eks-cluster'),
- 'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'create-a-new-certificate-based-eks-cluster'),
+ 'account-and-external-ids-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
+ 'create-role-arn-help-path' => help_page_path('user/project/clusters/add_eks_clusters.md', anchor: 'how-to-create-a-new-cluster-on-eks-through-cluster-certificates-deprecated'),
'external-link-icon' => sprite_icon('external-link') } }
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 0a482f1eb0..2a09d8d8cc 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -15,7 +15,7 @@
provider_type: @cluster.provider_type,
help_path: help_page_path('user/project/clusters/index.md'),
environments_help_path: help_page_path('ci/environments/index.md', anchor: 'create-a-static-environment'),
- clusters_help_path: help_page_path('user/project/clusters/index.md', anchor: 'deploying-to-a-kubernetes-cluster'),
+ clusters_help_path: help_page_path('user/project/clusters/deploy_to_cluster.md'),
deploy_boards_help_path: help_page_path('user/project/deploy_boards.md', anchor: 'enabling-deploy-boards'),
cluster_id: @cluster.id } }
@@ -24,11 +24,10 @@
.js-serverless-survey-banner{ data: { user_name: current_user.name, user_email: current_user.email } }
- .d-flex.my-3
- %p.badge.badge-light.p-2.mr-2
+ %h4.gl-my-5
+ = @cluster.name
+ %span.badge.badge-info.badge-pill.gl-badge.md.gl-vertical-align-middle
= cluster_type_label(@cluster.cluster_type)
- %h4.m-0
- = @cluster.name
= render 'banner'
@@ -42,12 +41,12 @@
- if cluster_created?(@cluster)
.js-toggle-container
- %ul.nav-links.mobile-separator.nav.nav-tabs{ role: 'tablist' }
- = render 'details_tab'
+ = gl_tabs_nav do
+ = render 'clusters/clusters/details_tab'
= render_if_exists 'clusters/clusters/environments_tab'
= render 'clusters/clusters/health_tab'
- = render 'integrations_tab'
- = render 'advanced_settings_tab'
+ = render 'clusters/clusters/integrations_tab'
+ = render 'clusters/clusters/advanced_settings_tab'
.tab-content.py-3
.tab-pane.active{ role: 'tabpanel' }
diff --git a/app/views/dashboard/_activity_head.html.haml b/app/views/dashboard/_activity_head.html.haml
index 0daadd20f5..c65b947d1b 100644
--- a/app/views/dashboard/_activity_head.html.haml
+++ b/app/views/dashboard/_activity_head.html.haml
@@ -2,13 +2,7 @@
%h1.page-title= _('Activity')
.top-area
- %ul.nav-links.nav.nav-tabs
- %li{ class: active_when(params[:filter].nil?) }>
- = link_to activity_dashboard_path, class: 'shortcuts-activity', data: {placement: 'right'} do
- = _('Your projects')
- %li{ class: active_when(params[:filter] == 'starred') }>
- = link_to activity_dashboard_path(filter: 'starred'), data: {placement: 'right'} do
- = _('Starred projects')
- %li{ class: active_when(params[:filter] == 'followed') }>
- = link_to activity_dashboard_path(filter: 'followed'), data: {placement: 'right'} do
- = _('Followed users')
+ = gl_tabs_nav({ class: 'gl-border-b-0', data: { testid: 'dashboard-activity-tabs' } }) do
+ = gl_tab_link_to _("Your projects"), activity_dashboard_path, { item_active: params[:filter].nil? }
+ = gl_tab_link_to _("Starred projects"), activity_dashboard_path(filter: 'starred')
+ = gl_tab_link_to _("Followed users"), activity_dashboard_path(filter: 'followed')
diff --git a/app/views/dashboard/_groups_head.html.haml b/app/views/dashboard/_groups_head.html.haml
index b92f35c108..7b1d25b9b4 100644
--- a/app/views/dashboard/_groups_head.html.haml
+++ b/app/views/dashboard/_groups_head.html.haml
@@ -6,13 +6,9 @@
= link_to _("New group"), new_group_path, class: "gl-button btn btn-confirm", data: { testid: "new-group-button" }
.top-area
- %ul.nav-links.mobile-separator.nav.nav-tabs
- = nav_link(page: dashboard_groups_path) do
- = link_to dashboard_groups_path, title: _("Your groups") do
- Your groups
- = nav_link(page: explore_groups_path) do
- = link_to explore_groups_path, title: _("Explore public groups") do
- Explore public groups
+ = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-0' }) do
+ = gl_tab_link_to _("Your groups"), dashboard_groups_path
+ = gl_tab_link_to _("Explore public groups"), explore_groups_path
.nav-controls
= render 'shared/groups/search_form'
= render 'shared/groups/dropdown'
diff --git a/app/views/devise/confirmations/almost_there.haml b/app/views/devise/confirmations/almost_there.haml
index 037b2f247c..9fb0fb734f 100644
--- a/app/views/devise/confirmations/almost_there.haml
+++ b/app/views/devise/confirmations/almost_there.haml
@@ -1,16 +1,15 @@
- user_email = "(#{params[:email]})" if params[:email].present?
+- request_link_start = ''.html_safe % { new_user_confirmation_path: new_user_confirmation_path }
+- request_link_end = ''.html_safe
.well-confirmation.gl-text-center.gl-mb-6
%h1.gl-mt-0
= _("Almost there...")
- %p.lead.gl-mb-6
+ %p{ class: 'gl-mb-6 gl-font-lg!' }
= _('Please check your email %{email} to confirm your account') % { email: user_email }
%hr
- if Gitlab::CurrentSettings.after_sign_up_text.present?
.well-confirmation.gl-text-center
= markdown_field(Gitlab::CurrentSettings, :after_sign_up_text)
-%p.text-center
- = _("No confirmation email received? Please check your spam folder or")
-.gl-mb-6.prepend-top-20.gl-text-center
- %a.gl-link{ href: new_user_confirmation_path }
- = _("Request new confirmation email")
+%p.gl-text-center
+ = _("No confirmation email received? Check your spam folder or %{request_link_start}request new confirmation email%{request_link_end}.").html_safe % { request_link_start: request_link_start, request_link_end: request_link_end }
diff --git a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
index ab46aaaca1..32e88047a9 100644
--- a/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
+++ b/app/views/devise/mailer/_confirmation_instructions_secondary.text.erb
@@ -1,4 +1,4 @@
-<%= _(" %{name}, confirm your email address now! ") % { name: @resource.user.name } %>
+<%= _("%{name}, confirm your email address now!") % { name: @resource.user.name } %>
<%= _("Use the link below to confirm your email address (%{email})") % { email: @resource.email } %>
diff --git a/app/views/devise/passwords/edit.html.haml b/app/views/devise/passwords/edit.html.haml
index 10c0442358..56bd30fac7 100644
--- a/app/views/devise/passwords/edit.html.haml
+++ b/app/views/devise/passwords/edit.html.haml
@@ -7,10 +7,10 @@
= f.hidden_field :reset_password_token
.form-group
= f.label _('New password'), for: "user_password"
- = f.password_field :password, class: "form-control gl-form-input top", required: true, title: _('This field is required.'), data: { qa_selector: 'password_field'}
+ = f.password_field :password, autocomplete: 'new-password', class: "form-control gl-form-input top", required: true, title: _('This field is required.'), data: { qa_selector: 'password_field'}
.form-group
= f.label _('Confirm new password'), for: "user_password_confirmation"
- = f.password_field :password_confirmation, class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { qa_selector: 'password_confirmation_field' }, required: true
+ = f.password_field :password_confirmation, autocomplete: 'new-password', class: "form-control gl-form-input bottom", title: _('This field is required.'), data: { qa_selector: 'password_confirmation_field' }, required: true
.clearfix
= f.submit _("Change your password"), class: "gl-button btn btn-confirm", data: { qa_selector: 'change_password_button' }
diff --git a/app/views/devise/registrations/new.html.haml b/app/views/devise/registrations/new.html.haml
index 4ec3fcde33..87108c8ea7 100644
--- a/app/views/devise/registrations/new.html.haml
+++ b/app/views/devise/registrations/new.html.haml
@@ -2,6 +2,7 @@
- add_page_specific_style 'page_bundles/signup'
- content_for :page_specific_javascripts do
= render "layouts/google_tag_manager_head"
+ = render "layouts/one_trust"
= render "layouts/google_tag_manager_body"
.signup-page
diff --git a/app/views/devise/sessions/_new_base.html.haml b/app/views/devise/sessions/_new_base.html.haml
index 82c0df354d..b40373ecc3 100644
--- a/app/views/devise/sessions/_new_base.html.haml
+++ b/app/views/devise/sessions/_new_base.html.haml
@@ -4,12 +4,12 @@
= f.text_field :login, value: @invite_email, class: 'form-control gl-form-input top', autofocus: 'autofocus', autocapitalize: 'off', autocorrect: 'off', required: true, title: _('This field is required.'), data: { qa_selector: 'login_field' }
.form-group
= f.label :password, class: 'label-bold'
- = f.password_field :password, class: 'form-control gl-form-input bottom', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
+ = f.password_field :password, class: 'form-control gl-form-input bottom', autocomplete: 'current-password', required: true, title: _('This field is required.'), data: { qa_selector: 'password_field' }
- if devise_mapping.rememberable?
- .remember-me
+ %div
%label{ for: 'user_remember_me' }
- = f.check_box :remember_me, class: 'remember-me-checkbox'
- %span Remember me
+ = f.check_box :remember_me
+ %span= _('Remember me')
.float-right
- if unconfirmed_email?
= link_to _('Resend confirmation email'), new_user_confirmation_path
diff --git a/app/views/devise/sessions/_new_crowd.html.haml b/app/views/devise/sessions/_new_crowd.html.haml
index 769268748f..fb4c011dd4 100644
--- a/app/views/devise/sessions/_new_crowd.html.haml
+++ b/app/views/devise/sessions/_new_crowd.html.haml
@@ -4,7 +4,7 @@
= text_field_tag :username, nil, { class: "form-control top", title: _("This field is required."), autofocus: "autofocus", required: true }
.form-group
= label_tag :password
- = password_field_tag :password, nil, { class: "form-control bottom", title: _("This field is required."), required: true }
+ = password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control bottom", title: _("This field is required."), required: true }
- if devise_mapping.rememberable?
.remember-me
%label{ for: "remember_me" }
diff --git a/app/views/devise/sessions/_new_ldap.html.haml b/app/views/devise/sessions/_new_ldap.html.haml
index f599a652b7..fea58779c1 100644
--- a/app/views/devise/sessions/_new_ldap.html.haml
+++ b/app/views/devise/sessions/_new_ldap.html.haml
@@ -8,7 +8,7 @@
= text_field_tag :username, nil, { class: "form-control gl-form-input top", title: _("This field is required."), autofocus: "autofocus", data: { qa_selector: 'username_field' }, required: true }
.form-group
= label_tag :password
- = password_field_tag :password, nil, { class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true }
+ = password_field_tag :password, nil, { autocomplete: 'current-password', class: "form-control gl-form-input bottom", title: _("This field is required."), data: { qa_selector: 'password_field' }, required: true }
- if !hide_remember_me && devise_mapping.rememberable?
.remember-me
%label{ for: "remember_me" }
diff --git a/app/views/devise/sessions/new.html.haml b/app/views/devise/sessions/new.html.haml
index 74f3e3e7e3..da6232b2a2 100644
--- a/app/views/devise/sessions/new.html.haml
+++ b/app/views/devise/sessions/new.html.haml
@@ -1,6 +1,7 @@
- page_title _("Sign in")
- content_for :page_specific_javascripts do
= render "layouts/google_tag_manager_head"
+ = render "layouts/one_trust"
= render "layouts/google_tag_manager_body"
#signin-container
diff --git a/app/views/devise/shared/_omniauth_box.html.haml b/app/views/devise/shared/_omniauth_box.html.haml
index 1752a43b03..bd7fe41ae8 100644
--- a/app/views/devise/shared/_omniauth_box.html.haml
+++ b/app/views/devise/shared/_omniauth_box.html.haml
@@ -1,20 +1,20 @@
- hide_remember_me = local_assigns.fetch(:hide_remember_me, false)
.omniauth-container.gl-mt-5
- %label.label-bold.d-block
+ %label.gl-font-weight-bold
= _('Sign in with')
- providers = enabled_button_based_providers
- .d-flex.justify-content-between.flex-wrap
+ .gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
- has_icon = provider_has_icon?(provider)
- = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "btn gl-button btn-default omniauth-btn oauth-login #{qa_class_for_provider(provider)}", form: { class: 'gl-w-full' } do
+ = button_to omniauth_authorize_path(:user, provider), id: "oauth-login-#{provider}", class: "btn gl-button btn-default gl-w-full js-oauth-login #{qa_class_for_provider(provider)}", form: { class: 'gl-w-full gl-mb-3' } do
- if has_icon
= provider_image_tag(provider)
%span.gl-button-text
= label_for_provider(provider)
- unless hide_remember_me
- %fieldset.remember-me
+ %fieldset
%label
- = check_box_tag :remember_me, nil, false, class: 'remember-me-checkbox'
+ = check_box_tag :remember_me, nil, false
%span
= _('Remember me')
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index f964987553..15143684b8 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -53,6 +53,7 @@
= f.password_field :password,
class: 'form-control gl-form-input bottom',
data: { qa_selector: 'new_user_password_field' },
+ autocomplete: 'new-password',
required: true,
pattern: ".{#{@minimum_password_length},}",
title: s_('SignUp|Minimum length is %{minimum_password_length} characters.') % { minimum_password_length: @minimum_password_length }
diff --git a/app/views/devise/shared/_signup_omniauth_provider_list.haml b/app/views/devise/shared/_signup_omniauth_provider_list.haml
index 43e0802ee2..c24e8770f0 100644
--- a/app/views/devise/shared/_signup_omniauth_provider_list.haml
+++ b/app/views/devise/shared/_signup_omniauth_provider_list.haml
@@ -1,9 +1,9 @@
-%label.label-bold.d-block
+%label.gl-font-weight-bold
= _("Create an account using:")
-.d-flex.justify-content-between.flex-wrap
+.gl-display-flex.gl-justify-content-between.gl-flex-wrap
- providers.each do |provider|
- = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-display-flex gl-align-items-center gl-text-left gl-mb-2 gl-p-2 omniauth-btn oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
+ = link_to omniauth_authorize_path(:user, provider), method: :post, class: "btn gl-button btn-default gl-w-full gl-mb-3 js-oauth-login #{qa_class_for_provider(provider)}", id: "oauth-login-#{provider}" do
- if provider_has_icon?(provider)
= provider_image_tag(provider)
- %span.ml-2
+ %span.gl-button-text
= label_for_provider(provider)
diff --git a/app/views/devise/shared/_signup_omniauth_providers.haml b/app/views/devise/shared/_signup_omniauth_providers.haml
index a653d44d69..30a54ab86a 100644
--- a/app/views/devise/shared/_signup_omniauth_providers.haml
+++ b/app/views/devise/shared/_signup_omniauth_providers.haml
@@ -1,3 +1,3 @@
-.omniauth-divider.d-flex.align-items-center.text-center
+.omniauth-divider.gl-display-flex.gl-align-items-center
= _("or")
= render 'devise/shared/signup_omniauth_provider_list', providers: enabled_button_based_providers
diff --git a/app/views/devise/shared/_signup_omniauth_providers_top.haml b/app/views/devise/shared/_signup_omniauth_providers_top.haml
index 9a2629443e..8eb22c0b02 100644
--- a/app/views/devise/shared/_signup_omniauth_providers_top.haml
+++ b/app/views/devise/shared/_signup_omniauth_providers_top.haml
@@ -1,3 +1,3 @@
= render 'devise/shared/signup_omniauth_provider_list', providers: popular_enabled_button_based_providers
-.omniauth-divider.d-flex.align-items-center.text-center
+.omniauth-divider.gl-display-flex.gl-align-items-center
= _("or")
diff --git a/app/views/groups/_group_admin_settings.html.haml b/app/views/groups/_group_admin_settings.html.haml
index ea191449fe..ab6861b5f2 100644
--- a/app/views/groups/_group_admin_settings.html.haml
+++ b/app/views/groups/_group_admin_settings.html.haml
@@ -36,4 +36,4 @@
.offset-sm-2.col-sm-10
.form-check
= f.text_field :two_factor_grace_period, class: 'form-control'
- .form-text.text-muted= _("Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication")
+ .form-text.text-muted= _("Time (in hours) that users are allowed to skip forced configuration of two-factor authentication.")
diff --git a/app/views/groups/_home_panel.html.haml b/app/views/groups/_home_panel.html.haml
index 0352f366f5..e530d9c60b 100644
--- a/app/views/groups/_home_panel.html.haml
+++ b/app/views/groups/_home_panel.html.haml
@@ -5,17 +5,19 @@
.group-home-panel
.row.mb-3
.home-panel-title-row.col-md-12.col-lg-6.d-flex
- .avatar-container.rect-avatar.s64.home-panel-avatar.gl-mr-3.float-none
+ .avatar-container.rect-avatar.s64.home-panel-avatar.gl-flex-shrink-0.float-none{ class: 'gl-mr-3!' }
= group_icon(@group, class: 'avatar avatar-tile s64', width: 64, height: 64, itemprop: 'logo')
.d-flex.flex-column.flex-wrap.align-items-baseline
.d-inline-flex.align-items-baseline
- %h1.home-panel-title.gl-mt-3.gl-mb-2{ itemprop: 'name' }
+ %h1.home-panel-title.gl-mt-3.gl-mb-2.gl-ml-3{ itemprop: 'name' }
= @group.name
%span.visibility-icon.text-secondary.gl-ml-2.has-tooltip{ data: { container: 'body' }, title: visibility_icon_description(@group) }
= visibility_level_icon(@group.visibility_level, options: {class: 'icon'})
- .home-panel-metadata.text-secondary
- %span
- = _("Group ID: %{group_id}") % { group_id: @group.id }
+ .home-panel-metadata.text-secondary.gl-font-base.gl-font-weight-normal.gl-line-height-normal
+ - if can?(current_user, :read_group, @group)
+ - button_class = "btn gl-button btn-sm btn-tertiary btn-default-tertiary home-panel-metadata"
+ - button_text = s_('GroupPage|Group ID: %{group_id}') % { group_id: @group.id }
+ = clipboard_button(title: s_('GroupPage|Copy group ID'), text: @group.id, hide_button_icon: true, button_text: button_text, class: button_class, qa_selector: 'group_id_content', itemprop: 'identifier')
- if current_user
%span.gl-ml-3
= render 'shared/members/access_request_links', source: @group
diff --git a/app/views/groups/_import_group_from_another_instance_panel.html.haml b/app/views/groups/_import_group_from_another_instance_panel.html.haml
index 2b9277c67e..06a86c2465 100644
--- a/app/views/groups/_import_group_from_another_instance_panel.html.haml
+++ b/app/views/groups/_import_group_from_another_instance_panel.html.haml
@@ -1,16 +1,15 @@
= form_with url: configure_import_bulk_imports_path, class: 'group-form gl-show-field-errors' do |f|
.gl-border-l-solid.gl-border-r-solid.gl-border-gray-100.gl-border-1.gl-p-5
- %h4.gl-display-flex
- = s_('GroupsNew|Import groups from another instance of GitLab')
- %span.badge.badge-info.badge-pill.gl-badge.md.gl-ml-3
- = _('Beta')
+ .gl-display-flex.gl-align-items-center
+ %h4.gl-display-flex
+ = s_('GroupsNew|Import groups from another instance of GitLab')
+ = link_to _('History'), history_import_bulk_imports_path, class: 'gl-link gl-ml-auto'
.gl-alert.gl-alert-warning{ role: 'alert' }
= sprite_icon('warning', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
.gl-alert-body
- docs_link_start = ''.html_safe % { url: help_page_path('user/group/import/index.md') }
- - feedback_link_start = ''.html_safe
- - link_end = ''.html_safe
- = s_('GroupsNew|Not all related objects are migrated, as %{docs_link_start}described here%{docs_link_end}. Please %{feedback_link_start}leave feedback%{feedback_link_end} on this feature.').html_safe % { docs_link_start: docs_link_start, docs_link_end: link_end, feedback_link_start: feedback_link_start, feedback_link_end: link_end }
+ - docs_link_end = ''.html_safe
+ = s_('GroupsNew|Not all related objects are migrated. %{docs_link_start}More info%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: docs_link_end }
%p.gl-mt-3
= s_('GroupsNew|Provide credentials for another instance of GitLab to import your groups directly.')
.form-group.gl-display-flex.gl-flex-direction-column
diff --git a/app/views/groups/dependency_proxies/_url.html.haml b/app/views/groups/dependency_proxies/_url.html.haml
deleted file mode 100644
index 9a76da63a7..0000000000
--- a/app/views/groups/dependency_proxies/_url.html.haml
+++ /dev/null
@@ -1,12 +0,0 @@
-- proxy_url = @group.dependency_proxy_image_prefix
-
-%h5.prepend-top-20= _('Dependency proxy image prefix')
-
-.row
- .col-lg-8.col-md-12.input-group
- = text_field_tag :url, "#{proxy_url}", class: 'js-dependency-proxy-url form-control', readonly: true
- = clipboard_button(text: "#{proxy_url}", title: _("Copy %{proxy_url}") % { proxy_url: proxy_url })
-
-.row
- .col-12.help-block.gl-mt-3{ data: { qa_selector: 'dependency_proxy_count' } }
- = _('Contains %{count} blobs of images (%{size})') % { count: @blobs_count, size: number_to_human_size(@blobs_total_size) }
diff --git a/app/views/groups/dependency_proxies/show.html.haml b/app/views/groups/dependency_proxies/show.html.haml
index 177018af83..8936c4dcbb 100644
--- a/app/views/groups/dependency_proxies/show.html.haml
+++ b/app/views/groups/dependency_proxies/show.html.haml
@@ -1,30 +1,5 @@
- page_title _("Dependency Proxy")
+- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
-.settings-header
- %h4= _('Dependency proxy')
-
- %p
- - link_start = ''.html_safe % { url: help_page_path('user/packages/dependency_proxy/index') }
- = _('Create a local proxy for storing frequently used upstream images. %{link_start}Learn more%{link_end} about dependency proxies.').html_safe % { link_start: link_start, link_end: ''.html_safe }
-
-- if Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
- - if can?(current_user, :admin_dependency_proxy, @group)
- = form_for(@dependency_proxy, method: :put, url: group_dependency_proxy_path(@group)) do |f|
- .form-group
- %h5.prepend-top-20= _('Enable proxy')
- .js-dependency-proxy-toggle-area
- = render "shared/buttons/project_feature_toggle", is_checked: @dependency_proxy.enabled?, label: s_("DependencyProxy|Toggle Dependency Proxy"), data: { qa_selector: 'dependency_proxy_setting_toggle' } do
- = f.hidden_field :enabled, { class: 'js-project-feature-toggle-input'}
-
- - if @dependency_proxy.enabled
- = render 'groups/dependency_proxies/url'
-
- - else
- - if @dependency_proxy.enabled
- = render 'groups/dependency_proxies/url'
-- else
- .gl-alert.gl-alert-info
- .gl-alert-container
- = sprite_icon('information-o', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content
- = _('Dependency proxy feature is limited to public groups for now.')
+#js-dependency-proxy{ data: { group_path: @group.full_path,
+ dependency_proxy_available: dependency_proxy_available.to_s } }
diff --git a/app/views/groups/edit.html.haml b/app/views/groups/edit.html.haml
index 6e355d3120..420771257c 100644
--- a/app/views/groups/edit.html.haml
+++ b/app/views/groups/edit.html.haml
@@ -1,5 +1,5 @@
-- breadcrumb_title _("General Settings")
-- page_title _("General Settings")
+- breadcrumb_title _("General settings")
+- page_title _("General settings")
- @content_class = "limit-container-width" unless fluid_layout
- expanded = expanded_by_default?
@@ -13,6 +13,7 @@
= _('Collapse')
%p
= _('Update your group name, description, avatar, and visibility.')
+ = link_to s_('Learn more about groups.'), help_page_path('user/group/index')
.settings-content
= render 'groups/settings/general'
@@ -23,7 +24,7 @@
%button.btn.gl-button.js-settings-toggle{ type: 'button' }
= expanded ? _('Collapse') : _('Expand')
%p
- = _('Advanced permissions, Large File Storage and Two-Factor authentication settings.')
+ = _('Configure advanced permissions, Large File Storage, and two-factor authentication settings.')
.settings-content
= render 'groups/settings/permissions'
diff --git a/app/views/groups/issues.html.haml b/app/views/groups/issues.html.haml
index 1f746484b7..0c6776a603 100644
--- a/app/views/groups/issues.html.haml
+++ b/app/views/groups/issues.html.haml
@@ -6,7 +6,7 @@
= auto_discovery_link_tag(:atom, safe_params.merge(rss_url_options).to_h, title: "#{@group.name} issues")
- if Feature.enabled?(:vue_issues_list, @group, default_enabled: :yaml)
- .js-issues-list{ data: group_issues_list_data(@group, current_user, @issues) }
+ .js-issues-list{ data: group_issues_list_data(@group, current_user, @issues, @projects) }
- if @can_bulk_update
= render_if_exists 'shared/issuable/group_bulk_update_sidebar', group: @group, type: :issues
- else
diff --git a/app/views/groups/registry/repositories/index.html.haml b/app/views/groups/registry/repositories/index.html.haml
index fa6bd021e4..2901c8fa46 100644
--- a/app/views/groups/registry/repositories/index.html.haml
+++ b/app/views/groups/registry/repositories/index.html.haml
@@ -16,7 +16,8 @@
is_group_page: "true",
"group_path": @group.full_path,
"gid_prefix": container_repository_gid_prefix,
- character_error: @character_error.to_s,
+ connection_error: (!!@connection_error).to_s,
+ invalid_path_error: (!!@invalid_path_error).to_s,
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s } }
diff --git a/app/views/groups/runners/_runner.html.haml b/app/views/groups/runners/_runner.html.haml
index 66ffef9855..a76701ea5d 100644
--- a/app/views/groups/runners/_runner.html.haml
+++ b/app/views/groups/runners/_runner.html.haml
@@ -39,7 +39,7 @@
- if runner.group_type?
= _('n/a')
- else
- = runner.projects.count(:all)
+ = runner.runner_projects.count(:all)
.table-section.section-5
.table-mobile-header{ role: 'rowheader' }= _('Jobs')
diff --git a/app/views/groups/settings/_general.html.haml b/app/views/groups/settings/_general.html.haml
index 7a2d5c91af..ed76a9fe25 100644
--- a/app/views/groups/settings/_general.html.haml
+++ b/app/views/groups/settings/_general.html.haml
@@ -14,8 +14,9 @@
.row.gl-mt-3
.form-group.col-md-9
- = f.label :description, _('Group description (optional)'), class: 'label-bold'
+ = f.label :description, _('Group description'), class: 'label-bold'
= f.text_area :description, class: 'form-control', rows: 3, maxlength: 250
+ .form-text.text-muted= _('Optional.')
= render_if_exists 'shared/repository_size_limit_setting', form: f, type: :group
diff --git a/app/views/groups/settings/_lfs.html.haml b/app/views/groups/settings/_lfs.html.haml
index 1255a2901e..9f04b579a9 100644
--- a/app/views/groups/settings/_lfs.html.haml
+++ b/app/views/groups/settings/_lfs.html.haml
@@ -3,10 +3,10 @@
%h5= _('Large File Storage')
-%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
+%p= s_('%{docs_link_start}What is Large File Storage?%{docs_link_end}').html_safe % { docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :lfs_enabled,
_('Allow projects within this group to use Git LFS'),
- help_text: _('This setting can be overridden in each project.'),
+ help_text: _('Can be overridden in each project.'),
checkbox_options: { checked: @group.lfs_enabled?, data: { qa_selector: 'lfs_checkbox' } }
diff --git a/app/views/groups/settings/_permissions.html.haml b/app/views/groups/settings/_permissions.html.haml
index 8f428909e6..eb38aa4388 100644
--- a/app/views/groups/settings/_permissions.html.haml
+++ b/app/views/groups/settings/_permissions.html.haml
@@ -7,7 +7,7 @@
- if @group.root?
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :prevent_sharing_groups_outside_hierarchy,
- s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups.').html_safe % { group: link_to_group(@group) },
+ s_('GroupSettings|Prevent members from sending invitations to groups outside of %{group} and its subgroups').html_safe % { group: link_to_group(@group) },
help_text: prevent_sharing_groups_outside_hierarchy_help_text(@group),
checkbox_options: { disabled: !can_change_prevent_sharing_groups_outside_hierarchy?(@group) }
@@ -21,13 +21,13 @@
= f.gitlab_ui_checkbox_component :emails_disabled,
s_('GroupSettings|Disable email notifications'),
checkbox_options: { checked: @group.emails_disabled?, disabled: !can_disable_group_emails?(@group) },
- help_text: s_('GroupSettings|This setting will override user notification preferences for all members of the group, subgroups, and projects.')
+ help_text: s_('GroupSettings|Overrides user notification preferences for all members of the group, subgroups, and projects.')
.form-group.gl-mb-3
= f.gitlab_ui_checkbox_component :mentions_disabled,
s_('GroupSettings|Disable group mentions'),
checkbox_options: { checked: @group.mentions_disabled? },
- help_text: s_('GroupSettings|This setting will prevent group members from being notified if the group is mentioned.')
+ help_text: s_('GroupSettings|Prevents group members from being notified if the group is mentioned.')
= render 'groups/settings/project_access_token_creation', f: f, group: @group
= render_if_exists 'groups/settings/delayed_project_removal', f: f, group: @group
diff --git a/app/views/groups/settings/_two_factor_auth.html.haml b/app/views/groups/settings/_two_factor_auth.html.haml
index 8204cafcb4..f86bcb24e6 100644
--- a/app/views/groups/settings/_two_factor_auth.html.haml
+++ b/app/views/groups/settings/_two_factor_auth.html.haml
@@ -4,16 +4,16 @@
%h5= _('Two-factor authentication')
-%p= s_('Check the %{docs_link_start}documentation%{docs_link_end}.').html_safe % { docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
+%p= s_('%{docs_link_start}What is two-factor authentication?%{docs_link_end}').html_safe % { docs_link_start: docs_link_start, docs_link_end: ''.html_safe }
.form-group
= f.gitlab_ui_checkbox_component :require_two_factor_authentication,
- _('Require all users in this group to setup two-factor authentication'),
+ _('Require all users in this group to set up two-factor authentication'),
checkbox_options: { data: { qa_selector: 'require_2fa_checkbox' } }
.form-group
- = f.label :two_factor_grace_period, _('Time before enforced'), class: 'label-bold'
- = f.text_field :two_factor_grace_period, class: 'form-control form-control-sm w-auto'
- .form-text.text-muted= _('Amount of time (in hours) that users are allowed to skip forced configuration of two-factor authentication')
+ = f.label :two_factor_grace_period, _('Time before enforced')
+ = f.text_field :two_factor_grace_period, class: 'form-control form-control-sm w-auto gl-form-input gl-mb-3'
+ .form-text.text-muted= _('Time (in hours) that users are allowed to skip forced configuration of two-factor authentication.')
- unless group.has_parent?
.form-group
= f.gitlab_ui_checkbox_component :allow_mfa_for_subgroups,
diff --git a/app/views/groups/settings/packages_and_registries/show.html.haml b/app/views/groups/settings/packages_and_registries/show.html.haml
index 1a12ad4902..7be6dc73c4 100644
--- a/app/views/groups/settings/packages_and_registries/show.html.haml
+++ b/app/views/groups/settings/packages_and_registries/show.html.haml
@@ -1,5 +1,8 @@
- breadcrumb_title _('Packages & Registries')
- page_title _('Packages & Registries')
- @content_class = 'limit-container-width' unless fluid_layout
+- dependency_proxy_available = Feature.enabled?(:dependency_proxy_for_private_groups, default_enabled: true) || @group.public?
-%section#js-packages-and-registries-settings{ data: { default_expanded: expanded_by_default?.to_s, group_path: @group.full_path } }
+%section#js-packages-and-registries-settings{ data: { default_expanded: expanded_by_default?.to_s,
+ group_path: @group.full_path,
+ dependency_proxy_available: dependency_proxy_available.to_s } }
diff --git a/app/views/help/instance_configuration/_gitlab_pages.html.haml b/app/views/help/instance_configuration/_gitlab_pages.html.haml
index 51835c202d..1d8958c93e 100644
--- a/app/views/help/instance_configuration/_gitlab_pages.html.haml
+++ b/app/views/help/instance_configuration/_gitlab_pages.html.haml
@@ -7,7 +7,7 @@
= _('GitLab Pages')
%p
- - link_to_gitlab_pages = link_to(_('GitLab Pages'), gitlab_pages[:url], target: '_blank')
+ - link_to_gitlab_pages = link_to(_('GitLab Pages'), gitlab_pages[:url], target: '_blank', rel: 'noopener noreferrer')
= _('Below are the settings for %{link_to_gitlab_pages}.').html_safe % { link_to_gitlab_pages: link_to_gitlab_pages }
.table-responsive
%table
diff --git a/app/views/help/instance_configuration/_rate_limits.html.haml b/app/views/help/instance_configuration/_rate_limits.html.haml
index d72bd845c5..ed71b5a609 100644
--- a/app/views/help/instance_configuration/_rate_limits.html.haml
+++ b/app/views/help/instance_configuration/_rate_limits.html.haml
@@ -24,6 +24,7 @@
= render 'help/instance_configuration/rate_limit_row', title: _('Protected Paths: requests'), rate_limit: rate_limits[:protected_paths]
= render 'help/instance_configuration/rate_limit_row', title: _('Package Registry: unauthenticated API requests'), rate_limit: rate_limits[:unauthenticated_packages_api], public_visible: true
= render 'help/instance_configuration/rate_limit_row', title: _('Package Registry: authenticated API requests'), rate_limit: rate_limits[:authenticated_packages_api]
+ = render 'help/instance_configuration/rate_limit_row', title: _('Authenticated Git LFS requests'), rate_limit: rate_limits[:authenticated_git_lfs_api]
= render 'help/instance_configuration/rate_limit_row', title: _('Issue creation requests'), rate_limit: rate_limits[:issue_creation]
= render 'help/instance_configuration/rate_limit_row', title: _('Note creation requests'), rate_limit: rate_limits[:note_creation]
= render 'help/instance_configuration/rate_limit_row', title: _('Project export requests'), rate_limit: rate_limits[:project_export]
diff --git a/app/views/import/_githubish_status.html.haml b/app/views/import/_githubish_status.html.haml
index 02a8f3142c..8f18d68fd5 100644
--- a/app/views/import/_githubish_status.html.haml
+++ b/app/views/import/_githubish_status.html.haml
@@ -6,7 +6,7 @@
- provider_title = Gitlab::ImportSources.title(provider)
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
#import-projects-mount-element{ data: { provider: provider, provider_title: provider_title,
can_select_namespace: current_user.can_select_namespace?.to_s,
diff --git a/app/views/import/bitbucket_server/new.html.haml b/app/views/import/bitbucket_server/new.html.haml
index ce6bdd7a2f..721447186a 100644
--- a/app/views/import/bitbucket_server/new.html.haml
+++ b/app/views/import/bitbucket_server/new.html.haml
@@ -1,6 +1,6 @@
- page_title _('Bitbucket Server Import')
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/import/bulk_imports/history.html.haml b/app/views/import/bulk_imports/history.html.haml
new file mode 100644
index 0000000000..80eb0c7a76
--- /dev/null
+++ b/app/views/import/bulk_imports/history.html.haml
@@ -0,0 +1,6 @@
+- add_to_breadcrumbs _('New group'), new_group_path
+- add_to_breadcrumbs _('Import group'), new_group_path(anchor: 'import-group-pane')
+- add_page_specific_style 'page_bundles/import'
+- page_title _('Import history')
+
+#import-history-mount-element
diff --git a/app/views/import/fogbugz/new.html.haml b/app/views/import/fogbugz/new.html.haml
index 5115679727..d716d08529 100644
--- a/app/views/import/fogbugz/new.html.haml
+++ b/app/views/import/fogbugz/new.html.haml
@@ -1,6 +1,6 @@
- page_title _("FogBugz Import")
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/import/fogbugz/new_user_map.html.haml b/app/views/import/fogbugz/new_user_map.html.haml
index 4281d77e83..93572e14a6 100644
--- a/app/views/import/fogbugz/new_user_map.html.haml
+++ b/app/views/import/fogbugz/new_user_map.html.haml
@@ -1,6 +1,6 @@
- page_title _('User map'), _('FogBugz import')
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/import/gitea/new.html.haml b/app/views/import/gitea/new.html.haml
index 288ae5f1ce..de717ce87e 100644
--- a/app/views/import/gitea/new.html.haml
+++ b/app/views/import/gitea/new.html.haml
@@ -1,6 +1,6 @@
- page_title _("Gitea Import")
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title
= custom_icon('gitea_logo')
diff --git a/app/views/import/github/new.html.haml b/app/views/import/github/new.html.haml
index 3407f9202b..3f7f929f76 100644
--- a/app/views/import/github/new.html.haml
+++ b/app/views/import/github/new.html.haml
@@ -1,7 +1,7 @@
- title = _('Authenticate with GitHub')
- page_title title
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title
= title
diff --git a/app/views/import/gitlab_projects/new.html.haml b/app/views/import/gitlab_projects/new.html.haml
index 028268482c..533d0d13be 100644
--- a/app/views/import/gitlab_projects/new.html.haml
+++ b/app/views/import/gitlab_projects/new.html.haml
@@ -1,6 +1,6 @@
- page_title _("GitLab Import")
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/import/manifest/new.html.haml b/app/views/import/manifest/new.html.haml
index bfaff3bb30..a949e14e27 100644
--- a/app/views/import/manifest/new.html.haml
+++ b/app/views/import/manifest/new.html.haml
@@ -1,6 +1,6 @@
- page_title _("Manifest file import")
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title
diff --git a/app/views/import/phabricator/new.html.haml b/app/views/import/phabricator/new.html.haml
index 9596fdb615..0249dc446e 100644
--- a/app/views/import/phabricator/new.html.haml
+++ b/app/views/import/phabricator/new.html.haml
@@ -1,6 +1,6 @@
- page_title _('Phabricator Server Import')
- header_title _("New project"), new_project_path
-- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_projects_path(anchor: 'import_project')
+- add_to_breadcrumbs s_('ProjectsNew|Import project'), new_project_path(anchor: 'import_project')
%h3.page-title.d-flex
.gl-display-flex.gl-align-items-center.gl-justify-content-center
diff --git a/app/views/layouts/_one_trust.html.haml b/app/views/layouts/_one_trust.html.haml
new file mode 100644
index 0000000000..4fab017d27
--- /dev/null
+++ b/app/views/layouts/_one_trust.html.haml
@@ -0,0 +1,16 @@
+- if one_trust_enabled?
+ - one_trust_id = sanitize(extra_config.one_trust_id, scrubber: Rails::Html::TextOnlyScrubber.new)
+
+
+ = javascript_include_tag "https://cdn.cookielaw.org/consent/#{one_trust_id}/OtAutoBlock.js"
+ = javascript_tag nonce: content_security_policy_nonce do
+ :plain
+ const oneTrustScript = document.createElement('script');
+ oneTrustScript.src = 'https://cdn.cookielaw.org/scripttemplates/otSDKStub.js';
+ oneTrustScript.dataset.domainScript = '#{one_trust_id}';
+ oneTrustScript.nonce = '#{content_security_policy_nonce}'
+ oneTrustScript.charset = 'UTF-8';
+ oneTrustScript.defer = true;
+ document.head.appendChild(oneTrustScript);
+
+ function OptanonWrapper() { }
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index ec2904245d..dff1b5e3d0 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -17,6 +17,7 @@
= render_two_factor_auth_recovery_settings_check
= render_if_exists "layouts/header/ee_subscribable_banner"
= render_if_exists "shared/namespace_storage_limit_alert"
+ = render_if_exists "shared/namespace_user_cap_reached_alert"
= render_if_exists "shared/new_user_signups_cap_reached_alert"
= yield :page_level_alert
= yield :customize_homepage_banner
diff --git a/app/views/layouts/_startup_js.html.haml b/app/views/layouts/_startup_js.html.haml
index 35cd191c60..0bf9c16b0d 100644
--- a/app/views/layouts/_startup_js.html.haml
+++ b/app/views/layouts/_startup_js.html.haml
@@ -8,20 +8,30 @@
if (gl.startup_calls && window.fetch) {
Object.keys(gl.startup_calls).forEach(apiCall => {
- // fetch won’t send cookies in older browsers, unless you set the credentials init option.
- // We set to `same-origin` which is default value in modern browsers.
- // See https://github.com/whatwg/fetch/pull/585 for more information.
- gl.startup_calls[apiCall] = {
- fetchCall: fetch(apiCall, { credentials: 'same-origin' })
+ gl.startup_calls[apiCall] = {
+ fetchCall: fetch(apiCall, {
+ // Emulate XHR for Rails AJAX request checks
+ headers: {
+ 'X-Requested-With': 'XMLHttpRequest'
+ },
+ // fetch won’t send cookies in older browsers, unless you set the credentials init option.
+ // We set to `same-origin` which is default value in modern browsers.
+ // See https://github.com/whatwg/fetch/pull/585 for more information.
+ credentials: 'same-origin'
+ })
};
});
}
if (gl.startup_graphql_calls && window.fetch) {
+ const headers = #{page_startup_graphql_headers.to_json};
const url = `#{api_graphql_url}`
const opts = {
method: "POST",
- headers: { "Content-Type": "application/json", 'X-CSRF-Token': "#{form_authenticity_token}" },
+ headers: {
+ "Content-Type": "application/json",
+ ...headers,
+ }
};
gl.startup_graphql_calls = gl.startup_graphql_calls.map(call => ({
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 3e7155b2c0..8d28823bfa 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -34,7 +34,8 @@
#js-header-search.header-search{ data: { 'search-context' => search_context.to_json,
'search-path' => search_path,
'issues-path' => issues_dashboard_path,
- 'mr-path' => merge_requests_dashboard_path } }
+ 'mr-path' => merge_requests_dashboard_path,
+ 'autocomplete-path' => search_autocomplete_path } }
%input{ type: "text", placeholder: _('Search or jump to...'), class: 'form-control gl-form-input' }
- else
= render 'layouts/search'
diff --git a/app/views/layouts/nav/_breadcrumbs.html.haml b/app/views/layouts/nav/_breadcrumbs.html.haml
index c111714f55..02a37dac15 100644
--- a/app/views/layouts/nav/_breadcrumbs.html.haml
+++ b/app/views/layouts/nav/_breadcrumbs.html.haml
@@ -19,8 +19,9 @@
= render "layouts/nav/breadcrumbs/collapsed_dropdown", location: :after
- unless @skip_current_level_breadcrumb
%li
- %h2.breadcrumbs-sub-title
+ %h2.breadcrumbs-sub-title{ data: { qa_selector: 'breadcrumb_sub_title_content' } }
= link_to @breadcrumb_title, breadcrumb_title_link
+ -# haml-lint:disable InlineJavaScript
%script{ type: 'application/ld+json' }
:plain
#{schema_breadcrumb_json}
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index d0b73a3364..842fb23d24 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -1,13 +1,13 @@
%aside.nav-sidebar.qa-admin-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), 'aria-label': _('Admin navigation') }
.nav-sidebar-inner-scroll
.context-header
- = link_to admin_root_path, title: _('Admin Overview') do
+ = link_to admin_root_path, title: _('Admin Overview'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
%span{ class: ['avatar-container', 'settings-avatar', 'rect-avatar', 's32'] }
= sprite_icon('admin', size: 18)
%span.sidebar-context-title
= _('Admin Area')
%ul.sidebar-top-level-items{ data: { qa_selector: 'admin_sidebar_overview_submenu_content' } }
- = nav_link(controller: %w(dashboard admin admin/projects users groups jobs runners gitaly_servers cohorts), html_options: {class: 'home'}) do
+ = nav_link(controller: %w(dashboard admin admin/projects users groups admin/topics jobs runners gitaly_servers cohorts), html_options: {class: 'home'}) do
= link_to admin_root_path, class: 'has-sub-items' do
.nav-icon-container
= sprite_icon('overview')
@@ -35,6 +35,10 @@
= link_to admin_groups_path, title: _('Groups'), data: { qa_selector: 'groups_overview_link' } do
%span
= _('Groups')
+ = nav_link(controller: [:admin, 'admin/topics']) do
+ = link_to admin_topics_path, title: _('Topics'), data: { qa_selector: 'topics_overview_link' } do
+ %span
+ = _('Topics')
= nav_link path: 'jobs#index' do
= link_to admin_jobs_path, title: _('Jobs') do
%span
@@ -257,11 +261,6 @@
= link_to ci_cd_admin_application_settings_path, title: _('CI/CD') do
%span
= _('CI/CD')
- - if Feature.enabled?(:serverless_domain)
- = nav_link(path: 'application_settings#operations') do
- = link_to admin_serverless_domains_path, title: _('Operations') do
- %span
- = _('Operations')
= nav_link(path: 'application_settings#reporting') do
= link_to reporting_admin_application_settings_path, title: _('Reporting') do
%span
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 4db1e532ba..16c0c00ad3 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -1,7 +1,7 @@
%aside.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?), **sidebar_tracking_attributes_by_object(current_user), 'aria-label': _('User settings') }
.nav-sidebar-inner-scroll
.context-header
- = link_to profile_path, title: _('Profile Settings') do
+ = link_to profile_path, title: _('Profile Settings'), class: 'has-tooltip', data: { container: 'body', placement: 'right' } do
%span{ class: ['avatar-container', 'settings-avatar', 's32'] }
= image_tag avatar_icon_for_user(current_user, 32), class: ['avatar', 'avatar-tile', 'js-sidebar-user-avatar', 's32'], alt: current_user.name, data: { testid: 'sidebar-user-avatar' }
%span.sidebar-context-title= _('User Settings')
diff --git a/app/views/profiles/_email_settings.html.haml b/app/views/profiles/_email_settings.html.haml
index bc678c2c42..1057e96f44 100644
--- a/app/views/profiles/_email_settings.html.haml
+++ b/app/views/profiles/_email_settings.html.haml
@@ -3,8 +3,11 @@
- email_change_disabled = local_assigns.fetch(:email_change_disabled, nil)
- read_only_help_text = readonly ? s_("Profiles|Your email address was automatically set based on your %{provider_label} account") % { provider_label: attribute_provider_label(:email) } : user_email_help_text(@user)
- help_text = email_change_disabled ? s_("Your account uses dedicated credentials for the \"%{group_name}\" group and can only be updated through SSO.") % { group_name: @user.managing_group.name } : read_only_help_text
+- password_automatically_set = @user.password_automatically_set?
= form.text_field :email, required: true, class: 'input-lg gl-form-input', value: (@user.email unless @user.temp_oauth_email?), help: help_text.html_safe, readonly: readonly || email_change_disabled
+- unless password_automatically_set
+ = hidden_field_tag 'user[validation_password]', :validation_password, class: 'js-password-prompt-field', help: s_("Profiles|Enter your password to confirm the email change")
= form.select :public_email, options_for_select(@user.public_verified_emails, selected: @user.public_email),
{ help: s_("Profiles|This email will be displayed on your public profile"), include_blank: s_("Profiles|Do not show on profile") },
control_class: 'select2 input-lg', disabled: email_change_disabled
diff --git a/app/views/profiles/passwords/edit.html.haml b/app/views/profiles/passwords/edit.html.haml
index 2cc919fc70..5d3e072017 100644
--- a/app/views/profiles/passwords/edit.html.haml
+++ b/app/views/profiles/passwords/edit.html.haml
@@ -19,16 +19,16 @@
- unless @user.password_automatically_set?
.form-group
- = f.label :current_password, _('Current password'), class: 'label-bold'
- = f.password_field :current_password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
+ = f.label :password, _('Current password'), class: 'label-bold'
+ = f.password_field :password, required: true, autocomplete: 'current-password', class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
%p.form-text.text-muted
= _('You must provide your current password in order to change it.')
.form-group
- = f.label :password, _('New password'), class: 'label-bold'
- = f.password_field :password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'new_password_field' }
+ = f.label :new_password, _('New password'), class: 'label-bold'
+ = f.password_field :new_password, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'new_password_field' }
.form-group
= f.label :password_confirmation, _('Password confirmation'), class: 'label-bold'
- = f.password_field :password_confirmation, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
+ = f.password_field :password_confirmation, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
.gl-mt-3.gl-mb-3
= f.submit _('Save password'), class: "gl-button btn btn-confirm gl-mr-3", data: { qa_selector: 'save_password_button' }
- unless @user.password_automatically_set?
diff --git a/app/views/profiles/passwords/new.html.haml b/app/views/profiles/passwords/new.html.haml
index 7780ffe0cb..9154c94abb 100644
--- a/app/views/profiles/passwords/new.html.haml
+++ b/app/views/profiles/passwords/new.html.haml
@@ -14,18 +14,18 @@
- unless @user.password_automatically_set?
.form-group.row
.col-sm-2.col-form-label
- = f.label :current_password, _('Current password')
+ = f.label :password, _('Current password')
.col-sm-10
- = f.password_field :current_password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
+ = f.password_field :password, required: true, autocomplete: 'current-password', class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
.form-group.row
.col-sm-2.col-form-label
- = f.label :password, _('New password')
+ = f.label :new_password, _('New password')
.col-sm-10
- = f.password_field :password, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'new_password_field' }
+ = f.password_field :new_password, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'new_password_field' }
.form-group.row
.col-sm-2.col-form-label
= f.label :password_confirmation, _('Password confirmation')
.col-sm-10
- = f.password_field :password_confirmation, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
+ = f.password_field :password_confirmation, required: true, autocomplete: 'new-password', class: 'form-control gl-form-input', data: { qa_selector: 'confirm_password_field' }
.form-actions
= f.submit _('Set new password'), class: 'gl-button btn btn-confirm', data: { qa_selector: 'set_new_password_button' }
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 0bb4859dd1..3e41f107e0 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -5,7 +5,7 @@
- availability = availability_values
- custom_emoji = show_status_emoji?(@user.status)
-= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors' }, authenticity_token: true do |f|
+= bootstrap_form_for @user, url: profile_path, method: :put, html: { multipart: true, class: 'edit-user gl-mt-3 js-quick-submit gl-show-field-errors js-password-prompt-form', remote: true }, authenticity_token: true do |f|
= form_errors(@user)
.row.js-search-settings-section
@@ -80,7 +80,7 @@
%p= s_("Profiles|Set your local time zone")
.col-lg-8
%h5= _("Time zone")
- = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
+ = dropdown_tag(_("Select a time zone"), options: { toggle_class: 'gl-button btn js-timezone-dropdown input-lg gl-w-full!', title: _("Select a time zone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
%input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
.col-lg-12
%hr
@@ -124,9 +124,11 @@
.help-block
= s_("Profiles|Choose to show contributions of private projects on your public profile without any project, repository or organization information")
%hr
- = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3'
+ = f.submit s_("Profiles|Update profile settings"), class: 'gl-button btn btn-confirm gl-mr-3 js-password-prompt-btn'
= link_to _("Cancel"), user_path(current_user), class: 'gl-button btn btn-default btn-cancel'
+#password-prompt-modal
+
.modal.modal-profile-crop{ data: { cropper_css_path: ActionController::Base.helpers.stylesheet_path('lazy_bundles/cropper.css') } }
.modal-dialog
.modal-content
diff --git a/app/views/profiles/two_factor_auths/show.html.haml b/app/views/profiles/two_factor_auths/show.html.haml
index bd3cb7e60f..00df860895 100644
--- a/app/views/profiles/two_factor_auths/show.html.haml
+++ b/app/views/profiles/two_factor_auths/show.html.haml
@@ -50,7 +50,7 @@
- if current_password_required?
.form-group
= label_tag :current_password, _('Current password'), class: 'label-bold'
- = password_field_tag :current_password, nil, required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
+ = password_field_tag :current_password, nil, autocomplete: 'current-password', required: true, class: 'form-control gl-form-input', data: { qa_selector: 'current_password_field' }
%p.form-text.text-muted
= _('Your current password is required to register a two-factor authenticator app.')
.gl-mt-3
diff --git a/app/views/projects/_files.html.haml b/app/views/projects/_files.html.haml
index 597a22bf34..cdcc98552f 100644
--- a/app/views/projects/_files.html.haml
+++ b/app/views/projects/_files.html.haml
@@ -20,5 +20,6 @@
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout), project_buttons: true
#js-tree-list{ data: vue_file_list_data(project, ref) }
- - if can_edit_tree?
+ - if !Feature.enabled?(:new_dir_modal, default_enabled: :yaml) && can_edit_tree?
= render 'projects/blob/new_dir'
+
diff --git a/app/views/projects/_home_panel.html.haml b/app/views/projects/_home_panel.html.haml
index f2cee61884..1f2c16324f 100644
--- a/app/views/projects/_home_panel.html.haml
+++ b/app/views/projects/_home_panel.html.haml
@@ -45,11 +45,10 @@
- if can?(current_user, :download_code, @project)
= cache_if(cache_enabled, [@project, :download_code], expires_in: 1.minute) do
%nav.project-stats
- .nav-links.quick-links
- - if @project.empty_repo?
- = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
- - else
- = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
+ - if @project.empty_repo?
+ = render 'stat_anchor_list', anchors: @project.empty_repo_statistics_anchors
+ - else
+ = render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
.home-panel-home-desc.mt-1
- if @project.description.present?
diff --git a/app/views/projects/_import_project_pane.html.haml b/app/views/projects/_import_project_pane.html.haml
index 815a3cf696..81d9726fcd 100644
--- a/app/views/projects/_import_project_pane.html.haml
+++ b/app/views/projects/_import_project_pane.html.haml
@@ -83,7 +83,7 @@
.js-toggle-content.toggle-import-form{ class: ('hide' if active_tab != 'import') }
- = form_for @project, html: { class: 'new_project' } do |f|
+ = form_for @project, html: { class: 'new_project gl-show-field-errors' } do |f|
%hr
= render "shared/import_form", f: f
= render 'projects/new_project_fields', f: f, project_name_id: "import-url-name", hide_init_with_readme: true, track_label: track_label
diff --git a/app/views/projects/_new_project_fields.html.haml b/app/views/projects/_new_project_fields.html.haml
index fb7a7ef898..256c3ebad0 100644
--- a/app/views/projects/_new_project_fields.html.haml
+++ b/app/views/projects/_new_project_fields.html.haml
@@ -16,7 +16,12 @@
- if current_user.can_select_namespace?
- namespace_id = namespace_id_from(params)
- if Feature.enabled?(:paginatable_namespace_drop_down_for_project_creation, current_user, default_enabled: :yaml)
- .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path, namespace_id: namespace_id, root_url: root_url, track_label: track_label } }
+ .js-vue-new-project-url-select{ data: { namespace_full_path: GroupFinder.new(current_user).execute(id: namespace_id)&.full_path,
+ namespace_id: namespace_id,
+ root_url: root_url,
+ track_label: track_label,
+ user_namespace_full_path: current_user.namespace.full_path,
+ user_namespace_id: current_user.namespace.id } }
- else
.input-group-prepend.flex-shrink-0.has-tooltip{ title: root_url }
.input-group-text
@@ -53,15 +58,36 @@
= render 'shared/visibility_level', f: f, visibility_level: visibility_level.to_i, can_change_visibility_level: true, form_model: @project, with_label: false
- if !hide_init_with_readme
- .form-group.row.initialize-with-readme-setting
- %div{ :class => "col-sm-12" }
- .form-check
- = check_box_tag 'project[initialize_with_readme]', '1', true, class: 'form-check-input', data: { qa_selector: "initialize_with_readme_checkbox", track_label: "#{track_label}", track_action: "activate_form_input", track_property: "init_with_readme", track_value: "" }
- = label_tag 'project[initialize_with_readme]', class: 'form-check-label' do
- .option-title
- %strong= s_('ProjectsNew|Initialize repository with a README')
- .option-description
- = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
+ = f.label :project_configuration, class: 'label-bold' do
+ = s_('ProjectsNew|Project Configuration')
+
+ .form-group
+ .form-check.gl-mb-3
+ = check_box_tag 'project[initialize_with_readme]', '1', true, class: 'form-check-input', data: { qa_selector: 'initialize_with_readme_checkbox', track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_readme' }
+ = label_tag 'project[initialize_with_readme]', s_('ProjectsNew|Initialize repository with a README'), class: 'form-check-label'
+ .form-text.text-muted
+ = s_('ProjectsNew|Allows you to immediately clone this project’s repository. Skip this if you plan to push up an existing repository.')
+
+ - experiment(:new_project_sast_enabled, user: current_user) do |e|
+ - e.try do
+ .form-group
+ .form-check.gl-mb-3
+ = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
+ = label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
+ = s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
+ .form-text.text-muted
+ = s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
+ = link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
+ - e.try(:free_indicator) do
+ .form-group
+ .form-check.gl-mb-3
+ = check_box_tag 'project[initialize_with_sast]', '1', true, class: 'form-check-input', data: { track_experiment: e.name, track_label: track_label, track_action: 'activate_form_input', track_property: 'init_with_sast' }
+ = label_tag 'project[initialize_with_sast]', class: 'form-check-label' do
+ = s_('ProjectsNew|Enable Static Application Security Testing (SAST)')
+ %span.badge.badge-info.badge-pill.gl-badge.sm= _('Free')
+ .form-text.text-muted
+ = s_('ProjectsNew|Analyze your source code for known security vulnerabilities.')
+ = link_to _('Learn more.'), help_page_path('user/application_security/sast/index'), target: '_blank', rel: 'noopener noreferrer', data: { track_action: 'followed', track_experiment: e.name }
= f.submit _('Create project'), class: "btn gl-button btn-confirm", data: { track_label: "#{track_label}", track_action: "click_button", track_property: "create_project", track_value: "" }
= link_to _('Cancel'), dashboard_projects_path, class: 'btn gl-button btn-default btn-cancel', data: { track_label: "#{track_label}", track_action: "click_button", track_property: "cancel", track_value: "" }
diff --git a/app/views/projects/blob/show.html.haml b/app/views/projects/blob/show.html.haml
index 66e9badbaf..168b240c65 100644
--- a/app/views/projects/blob/show.html.haml
+++ b/app/views/projects/blob/show.html.haml
@@ -18,3 +18,4 @@
= render 'projects/blob/upload', title: title, placeholder: title, button_title: 'Replace file', form_path: project_update_blob_path(@project, @id), method: :put
= render partial: 'pipeline_tour_success' if show_suggest_pipeline_creation_celebration?
+= render 'shared/web_ide_path'
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index b1d465d0e7..6733db69c3 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,19 +1,12 @@
- page_title _('Branches')
- add_to_breadcrumbs(_('Repository'), project_tree_path(@project))
-.top-area.adjust
- %ul.nav-links.issues-state-filters.nav.nav-tabs
- %li{ class: active_when(@mode == 'overview') }>
- = link_to s_('Branches|Overview'), project_branches_path(@project), title: s_('Branches|Show overview of the branches')
-
- %li{ class: active_when(@mode == 'active') }>
- = link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), title: s_('Branches|Show active branches')
-
- %li{ class: active_when(@mode == 'stale') }>
- = link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), title: s_('Branches|Show stale branches')
-
- %li{ class: active_when(!%w[overview active stale].include?(@mode)) }>
- = link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), title: s_('Branches|Show all branches')
+.top-area.gl-border-0
+ = gl_tabs_nav({ class: 'gl-flex-grow-1 gl-border-b-0' }) do
+ = gl_tab_link_to s_('Branches|Overview'), project_branches_path(@project), { item_active: @mode == 'overview', title: s_('Branches|Show overview of the branches') }
+ = gl_tab_link_to s_('Branches|Active'), project_branches_filtered_path(@project, state: 'active'), { title: s_('Branches|Show active branches') }
+ = gl_tab_link_to s_('Branches|Stale'), project_branches_filtered_path(@project, state: 'stale'), { title: s_('Branches|Show stale branches') }
+ = gl_tab_link_to s_('Branches|All'), project_branches_filtered_path(@project, state: 'all'), { item_active: !%w[overview active stale].include?(@mode), title: s_('Branches|Show all branches') }
.nav-controls
#js-branches-sort-dropdown{ data: { project_branches_filtered_path: project_branches_path(@project, state: 'all'), sort_options: branches_sort_options_hash.to_json, mode: @mode } }
@@ -38,7 +31,10 @@
%h5
= s_('Branches|Protected branches can be managed in %{project_settings_link}.').html_safe % { project_settings_link: project_settings_link }
-- if @mode == 'overview' && (@active_branches.any? || @stale_branches.any?)
+- if @gitaly_unavailable
+ = render 'shared/errors/gitaly_unavailable', reason: s_('Branches|Unable to load branches')
+
+- elsif @mode == 'overview' && (@active_branches.any? || @stale_branches.any?)
= render "projects/branches/panel", branches: @active_branches, state: 'active', panel_title: s_('Branches|Active branches'), show_more_text: s_('Branches|Show more active branches'), project: @project, overview_max_branches: @overview_max_branches
= render "projects/branches/panel", branches: @stale_branches, state: 'stale', panel_title: s_('Branches|Stale branches'), show_more_text: s_('Branches|Show more stale branches'), project: @project, overview_max_branches: @overview_max_branches
diff --git a/app/views/projects/branches/new.html.haml b/app/views/projects/branches/new.html.haml
index 27858932e5..8ee7910de4 100644
--- a/app/views/projects/branches/new.html.haml
+++ b/app/views/projects/branches/new.html.haml
@@ -31,4 +31,5 @@
.form-actions
= button_tag 'Create branch', class: 'gl-button btn btn-confirm'
= link_to _('Cancel'), project_branches_path(@project), class: 'gl-button btn btn-default btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/ci/pipeline_editor/show.html.haml b/app/views/projects/ci/pipeline_editor/show.html.haml
index 674765e9f8..ce6f7553ab 100644
--- a/app/views/projects/ci/pipeline_editor/show.html.haml
+++ b/app/views/projects/ci/pipeline_editor/show.html.haml
@@ -1,3 +1,5 @@
+- add_page_specific_style 'page_bundles/pipelines'
+
- page_title s_('Pipelines|Pipeline Editor')
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco')
diff --git a/app/views/projects/cluster_agents/show.html.haml b/app/views/projects/cluster_agents/show.html.haml
new file mode 100644
index 0000000000..a2d3426d99
--- /dev/null
+++ b/app/views/projects/cluster_agents/show.html.haml
@@ -0,0 +1,4 @@
+- add_to_breadcrumbs _('Kubernetes'), project_clusters_path(@project)
+- page_title @agent_name
+
+#js-cluster-agent-details{ data: js_cluster_agent_details_data(@agent_name, @project) }
diff --git a/app/views/projects/commits/_commit.html.haml b/app/views/projects/commits/_commit.html.haml
index bc0d14743b..62ed50f5a0 100644
--- a/app/views/projects/commits/_commit.html.haml
+++ b/app/views/projects/commits/_commit.html.haml
@@ -39,8 +39,14 @@
.committer
- commit_author_link = commit_author_link(commit, avatar: false, size: 24)
- - commit_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
- - commit_text = _('%{commit_author_link} authored %{commit_timeago}') % { commit_author_link: commit_author_link, commit_timeago: commit_timeago }
+ - commit_authored_timeago = time_ago_with_tooltip(commit.authored_date, placement: 'bottom')
+ - if commit.different_committer? && commit.committer
+ - commit_committer_link = commit_committer_link(commit)
+ - commit_committer_timeago = time_ago_with_tooltip(commit.committed_date, placement: 'bottom')
+ - commit_committer_avatar = commit_committer_avatar(commit.committer, size: 18, has_tooltip: false)
+ - commit_text = _('%{commit_author_link} authored %{commit_authored_timeago} and %{commit_committer_avatar} %{commit_committer_link} committed %{commit_committer_timeago}') % { commit_author_link: commit_author_link, commit_authored_timeago: commit_authored_timeago, commit_committer_avatar: commit_committer_avatar, commit_committer_link: commit_committer_link, commit_committer_timeago: commit_committer_timeago }
+ - else
+ - commit_text = _('%{commit_author_link} authored %{commit_authored_timeago}') % { commit_author_link: commit_author_link, commit_authored_timeago: commit_authored_timeago }
#{ commit_text.html_safe }
= render_if_exists 'projects/commits/project_namespace', show_project_name: show_project_name, project: project
diff --git a/app/views/projects/deployments/_deployment.html.haml b/app/views/projects/deployments/_deployment.html.haml
index 8270477ed3..57dfcb8cf4 100644
--- a/app/views/projects/deployments/_deployment.html.haml
+++ b/app/views/projects/deployments/_deployment.html.haml
@@ -27,7 +27,7 @@
= link_to deployment_path(deployment), class: 'build-link' do
#{deployment.deployable.name} (##{deployment.deployable.id})
- else
- .badge.badge-info.suggestion-help-hover{ title: s_('Deployment|This deployment was created using the API') }
+ .badge.badge-info.gl-cursor-help{ title: s_('Deployment|This deployment was created using the API') }
= s_('Deployment|API')
.table-section.section-10{ role: 'gridcell' }
diff --git a/app/views/projects/feature_flags/edit.html.haml b/app/views/projects/feature_flags/edit.html.haml
index c1c9f58265..ac8c057507 100644
--- a/app/views/projects/feature_flags/edit.html.haml
+++ b/app/views/projects/feature_flags/edit.html.haml
@@ -4,10 +4,4 @@
- breadcrumb_title @feature_flag.name
- page_title s_('FeatureFlags|Edit Feature Flag')
-#js-edit-feature-flag{ data: { endpoint: project_feature_flag_path(@project, @feature_flag),
- project_id: @project.id,
- feature_flags_path: project_feature_flags_path(@project),
- environments_endpoint: search_project_environments_path(@project, format: :json),
- strategy_type_docs_page_path: help_page_path('operations/feature_flags', anchor: 'feature-flag-strategies'),
- environments_scope_docs_path: help_page_path('ci/environments/index.md', anchor: 'scope-environments-with-specs'),
- feature_flag_issues_endpoint: feature_flag_issues_links_endpoint(@project, @feature_flag, current_user) } }
+#js-edit-feature-flag{ data: edit_feature_flag_data }
diff --git a/app/views/projects/google_cloud/index.html.haml b/app/views/projects/google_cloud/index.html.haml
new file mode 100644
index 0000000000..4fc66e1781
--- /dev/null
+++ b/app/views/projects/google_cloud/index.html.haml
@@ -0,0 +1,83 @@
+- breadcrumb_title _('Google Cloud')
+- page_title _('Google Cloud')
+
+- @content_class = "limit-container-width" unless fluid_layout
+
+#js-google-cloud
+
+ %h1.gl-font-size-h1 Google Cloud
+
+ %section#js-section-google-cloud-service-accounts
+
+ %h2.gl-font-size-h2 Service Accounts
+
+ %p= _('Service Accounts keys are required to authorize GitLab to deploy your Google Cloud project.')
+
+ %table.table.b-table.gl-table
+
+ %thead
+ %tr
+ %th Environment
+ %th GCP Project ID
+ %th Service Account Key
+
+ %tbody
+
+ %tr
+ %td *
+ %td serving-salutes-453
+ %td .....
+
+ %tr
+ %td production
+ %td crimson-corey-234
+ %td .....
+
+ %tr
+ %td review/*
+ %td roving-river-379
+ %td .....
+
+ %a.gl-button.btn.btn-primary= _('Add new service account')
+
+ %br
+
+ %section#js-section-google-cloud-deployments
+
+ .row.row-fluid
+
+ .col-lg-4
+ %h2.gl-font-size-h2 Deployments
+ %p= _('Google Cloud offers several deployment targets. Select the one most suitable for your project.')
+ %p
+ = _('Deployments to Google Kubernetes Engine can be ')
+ %a{ href: '#' }= _('managed')
+ = _('in Infrastructure :: Kubernetes clusters')
+
+ .col-lg-8
+
+ %br
+
+ .gl-card.gl-mb-6
+ .gl-card-body
+ .gl-display-flex.gl-align-items-baseline
+ %strong.gl-font-lg App Engine
+ .gl-ml-auto.gl-text-gray-500 Disabled
+ %p= _('App Engine description and apps that are suitable for this deployment target')
+ %button.gl-button.btn.btn-default= _('Configure via Merge Request')
+
+ .gl-card.gl-mb-6
+ .gl-card-body
+ .gl-display-flex.gl-align-items-baseline
+ %strong.gl-font-lg Cloud Functions
+ .gl-ml-auto.gl-text-gray-500 Disabled
+ %p= _('Cloud Functions description and apps that are suitable for this deployment target')
+ %button.gl-button.btn.btn-default= _('Configure via Merge Request')
+
+ .gl-card.gl-mb-6
+ .gl-card-body
+ .gl-display-flex.gl-align-items-baseline
+ %strong.gl-font-lg Cloud Run
+ .gl-ml-auto.gl-text-gray-500 Disabled
+ %p= _('Cloud Run description and apps that are suitable for this deployment target')
+ %button.gl-button.btn.btn-default= _('Configure via Merge Request')
diff --git a/app/views/projects/hook_logs/_index.html.haml b/app/views/projects/hook_logs/_index.html.haml
index ee4dbf5c05..6a46b0b351 100644
--- a/app/views/projects/hook_logs/_index.html.haml
+++ b/app/views/projects/hook_logs/_index.html.haml
@@ -1,37 +1,11 @@
-.row.gl-mt-7.gl-mb-3
+- docs_link_url = help_page_path('user/project/integrations/webhooks', anchor: 'troubleshoot-webhooks')
+- link_start = ''.html_safe % { url: docs_link_url }
+- link_end = ''.html_safe
+
+.row.gl-mt-3.gl-mb-3
.col-lg-3
%h4.gl-mt-0
- Recent Deliveries
- %p When an event in GitLab triggers a webhook, you can use the request details to figure out if something went wrong.
+ = _('Recent events')
+ %p= _('GitLab events trigger webhooks. Use the request details of a webhook to help troubleshoot problems. %{link_start}How do I troubleshoot?%{link_end}').html_safe % { link_start: link_start, link_end: link_end }
.col-lg-9
- - if hook_logs.present?
- %table.table
- %thead
- %tr
- %th Status
- %th Trigger
- %th URL
- %th Elapsed time
- %th Request time
- %th
- - hook_logs.each do |hook_log|
- %tr
- %td
- = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
- %td.d-none.d-sm-block
- %span.badge.badge-gray.deploy-project-label
- = hook_log.trigger.singularize.titleize
- %td
- = truncate(hook_log.url, length: 50)
- %td.light
- #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
- %td.light
- = time_ago_with_tooltip(hook_log.created_at)
- %td
- = link_to 'View details', hook_log.present.details_path
-
- = paginate hook_logs, theme: 'gitlab'
-
- - else
- .settings-message.text-center
- You don't have any webhooks deliveries
+ = render partial: 'shared/hook_logs/recent_deliveries_table', locals: { hook: hook, hook_logs: hook_logs }
diff --git a/app/views/projects/hook_logs/show.html.haml b/app/views/projects/hook_logs/show.html.haml
index ebe179c345..86dfa1929d 100644
--- a/app/views/projects/hook_logs/show.html.haml
+++ b/app/views/projects/hook_logs/show.html.haml
@@ -5,8 +5,8 @@
.row.gl-mt-3.gl-mb-3
.col-lg-3
%h4.gl-mt-0
- Request details
+ = _("Request details")
.col-lg-9
- = link_to 'Resend Request', @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
+ = link_to _('Resend Request'), @hook_log.present.retry_path, method: :post, class: "btn gl-button btn-default float-right gl-ml-3"
= render partial: 'shared/hook_logs/content', locals: { hook_log: @hook_log }
diff --git a/app/views/projects/issues/_nav_btns.html.haml b/app/views/projects/issues/_nav_btns.html.haml
index 0d69f6f69a..8d16c3d978 100644
--- a/app/views/projects/issues/_nav_btns.html.haml
+++ b/app/views/projects/issues/_nav_btns.html.haml
@@ -5,11 +5,11 @@
- can_edit = can?(current_user, :admin_project, @project)
- notification_email = @current_user.present? ? @current_user.notification_email_or_default : nil
-.nav-controls.issues-nav-controls
+.nav-controls.issues-nav-controls.gl-font-size-0
- if show_feed_buttons
= render 'shared/issuable/feed_buttons'
- .js-csv-import-export-buttons{ data: { show_export_button: show_export_button.to_s, show_import_button: show_import_button.to_s, issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_issues_path(@project, request.query_parameters), import_csv_issues_path: import_csv_namespace_project_issues_path, container_class: 'gl-mr-3', can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project) } }
+ .js-csv-import-export-buttons{ data: { show_export_button: show_export_button.to_s, show_import_button: show_import_button.to_s, issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_issues_path(@project, request.query_parameters), import_csv_issues_path: import_csv_namespace_project_issues_path, container_class: 'gl-mr-3', can_edit: can_edit.to_s, project_import_jira_path: project_import_jira_path(@project), max_attachment_size: number_to_human_size(Gitlab::CurrentSettings.max_attachment_size.megabytes) } }
- if @can_bulk_update
= button_tag _("Edit issues"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
@@ -18,4 +18,3 @@
issue: { milestone_id: finder.milestones.first.try(:id) }),
class: "gl-button btn btn-confirm",
id: "new_issue_link"
-
diff --git a/app/views/projects/issues/_related_branches.html.haml b/app/views/projects/issues/_related_branches.html.haml
index 0604e89be6..c47257eec4 100644
--- a/app/views/projects/issues/_related_branches.html.haml
+++ b/app/views/projects/issues/_related_branches.html.haml
@@ -1,9 +1,9 @@
- if @related_branches.any?
- %h2.related-branches-title
+ %h2.gl-font-lg
= pluralize(@related_branches.size, 'Related Branch')
%ul.unstyled-list.related-merge-requests
- @related_branches.each do |branch|
- %li
+ %li.gl-display-flex.gl-align-items-center
- if branch[:pipeline_status].present?
%span.related-branch-ci-status
= render 'ci/status/icon', status: branch[:pipeline_status]
diff --git a/app/views/projects/issues/_related_issues.html.haml b/app/views/projects/issues/_related_issues.html.haml
index d131d20f07..bab37609c2 100644
--- a/app/views/projects/issues/_related_issues.html.haml
+++ b/app/views/projects/issues/_related_issues.html.haml
@@ -3,4 +3,3 @@
can_add_related_issues: "#{can?(current_user, :admin_issue_link, @issue)}",
help_path: help_page_path('user/project/issues/related_issues'),
show_categorized_issues: "false" } }
- - render('projects/issues/related_issues_block')
diff --git a/app/views/projects/issues/_related_issues_block.html.haml b/app/views/projects/issues/_related_issues_block.html.haml
deleted file mode 100644
index 8d986b64b1..0000000000
--- a/app/views/projects/issues/_related_issues_block.html.haml
+++ /dev/null
@@ -1,5 +0,0 @@
-.related-issues-block
- .card.card-slim
- .card-header.panel-empty-heading.border-bottom-0
- %h3.card-title.mt-0.mb-0.h5
- = _('Linked issues')
diff --git a/app/views/projects/merge_requests/_mr_title.html.haml b/app/views/projects/merge_requests/_mr_title.html.haml
index 5a983fb556..0e8de3c2bb 100644
--- a/app/views/projects/merge_requests/_mr_title.html.haml
+++ b/app/views/projects/merge_requests/_mr_title.html.haml
@@ -32,20 +32,20 @@
.dropdown-menu.dropdown-menu-right
%ul
- if can_update_merge_request
- %li= link_to 'Edit', edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
+ %li= link_to _('Edit'), edit_namespace_project_merge_request_path(@project.namespace, @project, @merge_request)
- if @merge_request.opened?
%li
= link_to @merge_request.work_in_progress? ? _('Mark as ready') : _('Mark as draft'), toggle_draft_merge_request_path(@merge_request), method: :put, class: "js-draft-toggle-button"
%li{ class: [merge_request_button_visibility(@merge_request, true), 'js-close-item'] }
- = link_to 'Close', merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
+ = link_to _('Close'), merge_request_path(@merge_request, merge_request: { state_event: :close }), method: :put, title: 'Close merge request'
- if can_reopen_merge_request
%li{ class: merge_request_button_visibility(@merge_request, false) }
- = link_to 'Reopen', merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
+ = link_to _('Reopen'), merge_request_path(@merge_request, merge_request: { state_event: :reopen }), method: :put, title: 'Reopen merge request'
- unless @merge_request.merged? || current_user == @merge_request.author
- %li= link_to 'Report abuse', new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
+ %li= link_to _('Report abuse'), new_abuse_report_path(user_id: @merge_request.author.id, ref_url: merge_request_url(@merge_request))
- if can_update_merge_request
- = link_to 'Edit', edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
+ = link_to _('Edit'), edit_project_merge_request_path(@project, @merge_request), class: "d-none d-md-block btn gl-button btn-default btn-grouped js-issuable-edit", data: { qa_selector: "edit_button" }
- if can_update_merge_request && !are_close_and_open_buttons_hidden
= render 'projects/merge_requests/close_reopen_draft_report_toggle'
diff --git a/app/views/projects/merge_requests/_nav_btns.html.haml b/app/views/projects/merge_requests/_nav_btns.html.haml
index b34cf23634..00d12423eb 100644
--- a/app/views/projects/merge_requests/_nav_btns.html.haml
+++ b/app/views/projects/merge_requests/_nav_btns.html.haml
@@ -5,7 +5,7 @@
.js-csv-import-export-buttons{ data: { show_export_button: "true", issuable_type: issuable_type, issuable_count: issuables_count_for_state(issuable_type.to_sym, params[:state]), email: notification_email, export_csv_path: export_csv_project_merge_requests_path(@project, request.query_parameters), container_class: 'gl-mr-3' } }
- if @can_bulk_update
- = button_tag "Edit merge requests", class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
+ = button_tag _("Edit merge requests"), class: "gl-button btn btn-default gl-mr-3 js-bulk-update-toggle"
- if merge_project
- = link_to new_merge_request_path, class: "gl-button btn btn-confirm", title: "New merge request" do
- New merge request
+ = link_to new_merge_request_path, class: "gl-button btn btn-confirm", title: _("New merge request") do
+ = _('New merge request')
diff --git a/app/views/projects/merge_requests/_widget.html.haml b/app/views/projects/merge_requests/_widget.html.haml
index 47a0d05fc6..459742c3b8 100644
--- a/app/views/projects/merge_requests/_widget.html.haml
+++ b/app/views/projects/merge_requests/_widget.html.haml
@@ -19,6 +19,6 @@
window.gl.mrWidgetData.pipelines_empty_svg_path = '#{image_path('illustrations/pipelines_empty.svg')}';
window.gl.mrWidgetData.codequality_help_path = '#{help_page_path("user/project/merge_requests/code_quality", anchor: "code-quality-reports")}';
window.gl.mrWidgetData.false_positive_doc_url = '#{help_page_path('user/application_security/vulnerabilities/index')}';
- window.gl.mrWidgetData.can_view_false_positive = '#{(Feature.enabled?(:vulnerability_flags, default_enabled: :yaml) && @merge_request.project.licensed_feature_available?(:sast_fp_reduction)).to_s}';
+ window.gl.mrWidgetData.can_view_false_positive = '#{@merge_request.project.licensed_feature_available?(:sast_fp_reduction).to_s}';
#js-vue-mr-widget.mr-widget
diff --git a/app/views/projects/merge_requests/conflicts/show.html.haml b/app/views/projects/merge_requests/conflicts/show.html.haml
index ee296258d0..5ba42ca761 100644
--- a/app/views/projects/merge_requests/conflicts/show.html.haml
+++ b/app/views/projects/merge_requests/conflicts/show.html.haml
@@ -6,7 +6,7 @@
.merge-request-details.issuable-details
= render "projects/merge_requests/mr_box"
-= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, source_branch: @merge_request.source_branch
+= render 'shared/issuable/sidebar', issuable_sidebar: @issuable_sidebar, assignees: @merge_request.assignees, reviewers: @merge_request.reviewers, source_branch: @merge_request.source_branch
#conflicts{ data: { conflicts_path: conflicts_project_merge_request_path(@merge_request.project, @merge_request, format: :json),
resolve_conflicts_path: resolve_conflicts_project_merge_request_path(@merge_request.project, @merge_request),
diff --git a/app/views/projects/merge_requests/show.html.haml b/app/views/projects/merge_requests/show.html.haml
index 7e260a03c5..2154ef6b59 100644
--- a/app/views/projects/merge_requests/show.html.haml
+++ b/app/views/projects/merge_requests/show.html.haml
@@ -49,6 +49,7 @@
= render "projects/merge_requests/tabs/pane", id: "notes", class: "notes voting_notes" do
.row
%section.col-md-12
+ -# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(true).to_json.html_safe
.issuable-discussion.js-vue-notes-event
- if @merge_request.description.present?
@@ -98,3 +99,4 @@
= render 'projects/invite_members_modal', project: @project
- if Gitlab::CurrentSettings.gitpod_enabled && !current_user&.gitpod_enabled
= render 'shared/gitpod/enable_gitpod_modal'
+= render 'shared/web_ide_path'
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index b89aa9d402..d253ab8e32 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -10,7 +10,7 @@
= expanded ? _('Collapse') : _('Expand')
%p
= _('Set up your project to automatically push and/or pull changes to/from another repository. Branches, tags, and commits will be synced automatically.')
- = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/repository_mirroring'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('How do I mirror repositories?'), help_page_path('user/project/repository/mirror/index.md'), target: '_blank', rel: 'noopener noreferrer'
.settings-content
- if mirror_settings_enabled
@@ -32,7 +32,7 @@
= label_tag :only_protected_branches, _('Mirror only protected branches'), class: 'form-check-label'
.form-text.text-muted
= _('If enabled, only protected branches will be mirrored.')
- = link_to _('Learn more.'), help_page_path('user/project/repository/repository_mirroring', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
+ = link_to _('Learn more.'), help_page_path('user/project/repository/mirror/index.md', anchor: 'mirror-only-protected-branches'), target: '_blank', rel: 'noopener noreferrer'
.panel-footer
= f.submit _('Mirror repository'), class: 'gl-button btn btn-confirm js-mirror-submit qa-mirror-repository-button', name: :update_remote_mirror
diff --git a/app/views/projects/mirrors/_mirror_repos_push.html.haml b/app/views/projects/mirrors/_mirror_repos_push.html.haml
index b3e0f71bf1..339c5d8291 100644
--- a/app/views/projects/mirrors/_mirror_repos_push.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos_push.html.haml
@@ -12,4 +12,4 @@
= label_tag :keep_divergent_refs, _('Keep divergent refs'), class: 'form-check-label'
.form-text.text-muted
- link_opening_tag = ''.html_safe
- = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/repository_mirroring', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: ''.html_safe }
+ = html_escape(_('Do not force push over diverged refs. After the mirror is created, this setting can only be modified using the API. %{mirroring_docs_link_start}Learn more about this option%{link_closing_tag} and %{mirroring_api_docs_link_start}the API.%{link_closing_tag}')) % { mirroring_docs_link_start: link_opening_tag % {url: help_page_path('user/project/repository/mirror/push.md', anchor: 'keep-divergent-refs')}, mirroring_api_docs_link_start: link_opening_tag % {url: help_page_path('api/remote_mirrors')}, link_closing_tag: ''.html_safe }
diff --git a/app/views/projects/pipelines/_with_tabs.html.haml b/app/views/projects/pipelines/_with_tabs.html.haml
index 23606e2456..93afddce77 100644
--- a/app/views/projects/pipelines/_with_tabs.html.haml
+++ b/app/views/projects/pipelines/_with_tabs.html.haml
@@ -70,10 +70,10 @@
= link_to retry_project_job_path(build.project, build, return_to: request.original_url), method: :post, title: _('Retry'), class: 'gl-button btn btn-default btn-icon' do
= sprite_icon('repeat', css_class: 'gl-icon')
- if can?(current_user, :read_build, job)
- %tr.build-trace-row.responsive-table-border-end
+ %tr.build-log-row.responsive-table-border-end
%td
- %td.responsive-table-cell.build-trace-container{ colspan: 4 }
- %pre.build-trace.build-trace-rounded
+ %td.responsive-table-cell.build-log-container{ colspan: 4 }
+ %pre.build-log.build-log-rounded
%code.bash.js-build-output
= build_summary(build)
diff --git a/app/views/projects/product_analytics/test.html.haml b/app/views/projects/product_analytics/test.html.haml
index 60d897ee13..3204cd5fbb 100644
--- a/app/views/projects/product_analytics/test.html.haml
+++ b/app/views/projects/product_analytics/test.html.haml
@@ -12,5 +12,6 @@
%code
= @event.as_json_wo_empty
+-# haml-lint:disable InlineJavaScript
:javascript
#{render 'tracker'}
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index bdb5f021b7..cfdbf3410b 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -20,7 +20,8 @@
"is_admin": current_user&.admin.to_s,
"show_cleanup_policy_on_alert": show_cleanup_policy_on_alert(@project).to_s,
"cleanup_policies_settings_path": project_settings_packages_and_registries_path(@project),
- character_error: @character_error.to_s,
+ connection_error: (!!@connection_error).to_s,
+ invalid_path_error: (!!@invalid_path_error).to_s,
user_callouts_path: user_callouts_path,
user_callout_id: UserCalloutsHelper::UNFINISHED_TAG_CLEANUP_CALLOUT,
show_unfinished_tag_cleanup_callout: show_unfinished_tag_cleanup_callout?.to_s, } }
diff --git a/app/views/projects/settings/operations/_error_tracking.html.haml b/app/views/projects/settings/operations/_error_tracking.html.haml
index 6b2a1468ee..23b1ec4dea 100644
--- a/app/views/projects/settings/operations/_error_tracking.html.haml
+++ b/app/views/projects/settings/operations/_error_tracking.html.haml
@@ -18,4 +18,5 @@
api_host: setting.api_host,
enabled: setting.enabled.to_json,
integrated: setting.integrated.to_json,
+ gitlab_dsn: setting.gitlab_dsn,
token: setting.token.present? ? '*' * 12 : nil } }
diff --git a/app/views/projects/snippets/show.html.haml b/app/views/projects/snippets/show.html.haml
index 8ef53c40b1..3e6acdb130 100644
--- a/app/views/projects/snippets/show.html.haml
+++ b/app/views/projects/snippets/show.html.haml
@@ -3,7 +3,7 @@
- breadcrumb_title @snippet.to_reference
- page_title "#{@snippet.title} (#{@snippet.to_reference})", _("Snippets")
-#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true, api_awards_path: project_snippets_award_api_path(@snippet)
diff --git a/app/views/projects/tags/index.html.haml b/app/views/projects/tags/index.html.haml
index 79205a51d7..d3cc409df1 100644
--- a/app/views/projects/tags/index.html.haml
+++ b/app/views/projects/tags/index.html.haml
@@ -18,6 +18,9 @@
= render_if_exists 'projects/commits/mirror_status'
+ - if @tags_loading_error
+ = render 'shared/errors/gitaly_unavailable', reason: s_('TagsPage|Unable to load tags')
+
.tags
- if @tags.any?
%ul.flex-list.content-list
diff --git a/app/views/projects/tags/new.html.haml b/app/views/projects/tags/new.html.haml
index fe00772d1d..4281152225 100644
--- a/app/views/projects/tags/new.html.haml
+++ b/app/views/projects/tags/new.html.haml
@@ -54,4 +54,5 @@
.form-actions.gl-display-flex
= button_tag s_('TagsPage|Create tag'), class: 'gl-button btn btn-confirm gl-mr-3', data: { qa_selector: "create_tag_button" }
= link_to s_('TagsPage|Cancel'), project_tags_path(@project), class: 'gl-button btn btn-default btn-cancel'
+-# haml-lint:disable InlineJavaScript
%script#availableRefs{ type: "application/json" }= @project.repository.ref_names.to_json.html_safe
diff --git a/app/views/projects/tree/show.html.haml b/app/views/projects/tree/show.html.haml
index 2d0c4cc20a..1553eda1cf 100644
--- a/app/views/projects/tree/show.html.haml
+++ b/app/views/projects/tree/show.html.haml
@@ -11,3 +11,4 @@
= render 'projects/last_push'
= render 'projects/files', commit: @last_commit, project: @project, ref: @ref, content_url: project_tree_path(@project, @id)
+= render 'shared/web_ide_path'
diff --git a/app/views/projects/usage_quotas/index.html.haml b/app/views/projects/usage_quotas/index.html.haml
index dfd46af049..6c7cccfb9b 100644
--- a/app/views/projects/usage_quotas/index.html.haml
+++ b/app/views/projects/usage_quotas/index.html.haml
@@ -6,7 +6,7 @@
.row
.col-sm-12
= s_('UsageQuota|Usage of project resources across the %{strong_start}%{project_name}%{strong_end} project').html_safe % { strong_start: ''.html_safe, strong_end: ''.html_safe, project_name: @project.name } + '.'
- %a{ href: help_page_path('user/usage_quotas.md') }
+ %a{ href: help_page_path('user/usage_quotas.md'), target: '_blank', rel: 'noopener noreferrer' }
= s_('UsageQuota|Learn more about usage quotas') + '.'
.top-area.scrolling-tabs-container.inner-page-scroll-tabs
diff --git a/app/views/search/results/_issuable.html.haml b/app/views/search/results/_issuable.html.haml
index 5645fbfb23..41058034d6 100644
--- a/app/views/search/results/_issuable.html.haml
+++ b/app/views/search/results/_issuable.html.haml
@@ -13,7 +13,8 @@
= highlight_and_truncate_issuable(issuable, @search_term, @search_highlight)
.col-sm-3.gl-mt-3.gl-sm-mt-0.gl-text-right
- if issuable.respond_to?(:upvotes_count) && issuable.upvotes_count > 0
- %li.issuable-upvotes.gl-list-style-none.has-tooltip{ title: _('Upvotes') }
- = sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
- = issuable.upvotes_count
+ %li.issuable-upvotes.gl-list-style-none
+ %span.has-tooltip{ title: _('Upvotes') }
+ = sprite_icon('thumb-up', css_class: "gl-vertical-align-middle")
+ = issuable.upvotes_count
%span.gl-text-gray-500= sprintf(s_('updated %{time_ago}'), { time_ago: time_ago_with_tooltip(issuable.updated_at, placement: 'bottom') }).html_safe
diff --git a/app/views/shared/_import_form.html.haml b/app/views/shared/_import_form.html.haml
index f03314563c..3ab2b969b7 100644
--- a/app/views/shared/_import_form.html.haml
+++ b/app/views/shared/_import_form.html.haml
@@ -9,17 +9,12 @@
= f.text_field :import_url, value: import_url.sanitized_url,
autocomplete: 'off', class: 'form-control gl-form-input', placeholder: 'https://gitlab.company.com/group/project.git', required: true
= render 'shared/global_alert',
- variant: :warning,
- alert_class: 'gl-mt-3 js-import-url-warning hide',
+ variant: :danger,
+ alert_class: 'gl-mt-3 js-import-url-error hide',
dismissible: false,
close_button_class: 'js-close-2fa-enabled-success-alert' do
.gl-alert-body
- = s_('Import|A repository URL usually ends in a .git suffix, although this is not required. Double check to make sure your repository URL is correct.')
-
- .gl-alert.gl-alert-not-dismissible.gl-alert-warning.gl-mt-3.hide#project_import_url_warning
- .gl-alert-container
- = sprite_icon('warning-solid', css_class: 'gl-icon s16 gl-alert-icon gl-alert-icon-no-title')
- .gl-alert-content{ role: 'alert' }
+ = s_('Import|There is not a valid Git repository at this URL. If your HTTP repository is not publicly accessible, verify your credentials.')
.row
.form-group.col-md-6
= f.label :import_url_user, class: 'label-bold' do
diff --git a/app/views/shared/_milestones_filter.html.haml b/app/views/shared/_milestones_filter.html.haml
index eb50960202..117ed212fd 100644
--- a/app/views/shared/_milestones_filter.html.haml
+++ b/app/views/shared/_milestones_filter.html.haml
@@ -1,13 +1,15 @@
-%ul.nav-links.mobile-separator.nav.nav-tabs
- %li{ class: milestone_class_for_state(params[:state], 'opened', true) }>
- = link_to milestones_filter_path(state: 'opened') do
- = _('Open')
- %span.badge.badge-pill= counts[:opened]
- %li{ class: milestone_class_for_state(params[:state], 'closed') }>
- = link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc') do
- = _('Closed')
- %span.badge.badge-pill= counts[:closed]
- %li{ class: milestone_class_for_state(params[:state], 'all') }>
- = link_to milestones_filter_path(state: 'all', sort: 'due_date_desc') do
- = _('All')
- %span.badge.badge-pill= counts[:all]
+- count_badge_classes = 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex'
+
+= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'milestones-filter' } } ) do
+ = gl_tab_link_to milestones_filter_path(state: 'opened'), { item_active: params[:state].blank? || params[:state] == 'opened' } do
+ = _('Open')
+ %span{ class: count_badge_classes }
+ = counts[:opened]
+ = gl_tab_link_to milestones_filter_path(state: 'closed', sort: 'due_date_desc'), { item_active: params[:state] == 'closed' } do
+ = _('Closed')
+ %span{ class: count_badge_classes }
+ = counts[:closed]
+ = gl_tab_link_to milestones_filter_path(state: 'all', sort: 'due_date_desc'), { item_active: params[:state] == 'all' } do
+ = _('All')
+ %span{ class: count_badge_classes }
+ = counts[:all]
diff --git a/app/views/shared/_web_ide_path.html.haml b/app/views/shared/_web_ide_path.html.haml
new file mode 100644
index 0000000000..73d00bcd40
--- /dev/null
+++ b/app/views/shared/_web_ide_path.html.haml
@@ -0,0 +1,4 @@
+= javascript_tag do
+ :plain
+ window.gl = window.gl || {};
+ window.gl.webIDEPath = '#{web_ide_url}'
diff --git a/app/views/shared/builds/_build_output.html.haml b/app/views/shared/builds/_build_output.html.haml
index 380fac4d0e..a3b7d4926f 100644
--- a/app/views/shared/builds/_build_output.html.haml
+++ b/app/views/shared/builds/_build_output.html.haml
@@ -1,4 +1,4 @@
-%pre.build-trace#build-trace
+%pre.build-log
%code.bash.js-build-output
.build-loader-animation.js-build-refresh
.dot
diff --git a/app/views/shared/builds/_tabs.html.haml b/app/views/shared/builds/_tabs.html.haml
index 4973309edf..498e9cc33c 100644
--- a/app/views/shared/builds/_tabs.html.haml
+++ b/app/views/shared/builds/_tabs.html.haml
@@ -1,24 +1,19 @@
-%ul.nav-links.mobile-separator.nav.nav-tabs
- %li{ class: active_when(scope.nil?) }>
- = link_to build_path_proc.call(nil) do
- All
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm.js-totalbuilds-count
- = limited_counter_with_delimiter(all_builds)
+- count_badge_classes = 'badge badge-muted badge-pill gl-badge gl-tab-counter-badge sm gl-display-none gl-sm-display-inline-flex'
- %li{ class: active_when(scope == 'pending') }>
- = link_to build_path_proc.call('pending') do
- Pending
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = limited_counter_with_delimiter(all_builds.pending)
-
- %li{ class: active_when(scope == 'running') }>
- = link_to build_path_proc.call('running') do
- Running
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = limited_counter_with_delimiter(all_builds.running)
-
- %li{ class: active_when(scope == 'finished') }>
- = link_to build_path_proc.call('finished') do
- Finished
- %span.badge.gl-tab-counter-badge.badge-muted.badge-pill.gl-badge.sm
- = limited_counter_with_delimiter(all_builds.finished)
+= gl_tabs_nav( {class: 'gl-border-b-0 gl-flex-grow-1', data: { testid: 'jobs-tabs' } } ) do
+ = gl_tab_link_to build_path_proc.call(nil), { item_active: scope.nil? } do
+ = _('All')
+ %span{ class: count_badge_classes }
+ = limited_counter_with_delimiter(all_builds)
+ = gl_tab_link_to build_path_proc.call('pending'), { item_active: scope == 'pending' } do
+ = _('Pending')
+ %span{ class: count_badge_classes }
+ = limited_counter_with_delimiter(all_builds.pending)
+ = gl_tab_link_to build_path_proc.call('running'), { item_active: scope == 'running' } do
+ = _('Running')
+ %span{ class: count_badge_classes }
+ = limited_counter_with_delimiter(all_builds.running)
+ = gl_tab_link_to build_path_proc.call('finished'), { item_active: scope == 'finished' } do
+ = _('Finished')
+ %span{ class: count_badge_classes }
+ = limited_counter_with_delimiter(all_builds.finished)
diff --git a/app/views/shared/deploy_tokens/_form.html.haml b/app/views/shared/deploy_tokens/_form.html.haml
index 652da4b396..e049afbc40 100644
--- a/app/views/shared/deploy_tokens/_form.html.haml
+++ b/app/views/shared/deploy_tokens/_form.html.haml
@@ -4,7 +4,6 @@
= s_('DeployTokens|Create a new deploy token for all projects in this group. %{link_start}What are deploy tokens?%{link_end}').html_safe % { link_start: group_deploy_tokens_help_link_start, link_end: ''.html_safe }
= form_for token, url: create_deploy_token_path(group_or_project, anchor: 'js-deploy-tokens'), method: :post, remote: Feature.enabled?(:ajax_new_deploy_token, group_or_project) do |f|
- = form_errors(token)
.form-group
= f.label :name, class: 'label-bold'
diff --git a/app/views/shared/empty_states/_topics.html.haml b/app/views/shared/empty_states/_topics.html.haml
new file mode 100644
index 0000000000..fd82a85303
--- /dev/null
+++ b/app/views/shared/empty_states/_topics.html.haml
@@ -0,0 +1,7 @@
+.row.empty-state
+ .col-12
+ .svg-content
+ = image_tag 'illustrations/labels.svg', data: { qa_selector: 'svg_content' }
+ .text-content.gl-text-center.gl-pt-0!
+ %h4= _('There are no topics to show.')
+ %p= _('Add topics to projects to help users find them.')
diff --git a/app/views/shared/errors/_gitaly_unavailable.html.haml b/app/views/shared/errors/_gitaly_unavailable.html.haml
new file mode 100644
index 0000000000..96a68cbcdc
--- /dev/null
+++ b/app/views/shared/errors/_gitaly_unavailable.html.haml
@@ -0,0 +1,8 @@
+.gl-alert.gl-alert-danger.gl-mb-5.gl-mt-5
+ .gl-alert-container
+ = sprite_icon('error', size: 16, css_class: 'gl-icon gl-alert-icon gl-alert-icon-no-title')
+ .gl-alert-content
+ .gl-alert-title
+ = reason
+ .gl-alert-body
+ = s_('The git server, Gitaly, is not available at this time. Please contact your administrator.')
diff --git a/app/views/shared/hook_logs/_recent_deliveries_table.html.haml b/app/views/shared/hook_logs/_recent_deliveries_table.html.haml
new file mode 100644
index 0000000000..31ef856078
--- /dev/null
+++ b/app/views/shared/hook_logs/_recent_deliveries_table.html.haml
@@ -0,0 +1,34 @@
+%table.gl-table.gl-w-full
+ %thead
+ %tr
+ %th= _('Status')
+ %th.d-none.d-sm-table-cell= _('Trigger')
+ %th= _('Elapsed time')
+ %th= _('Request time')
+ %th
+
+ - if hook_logs.present?
+ - hook_logs.each do |hook_log|
+ %tr
+ %td
+ = render partial: 'shared/hook_logs/status_label', locals: { hook_log: hook_log }
+ %td.d-none.d-sm-table-cell
+ %span.badge.badge-pill.gl-badge.badge-muted.sm
+ = hook_log.trigger.singularize.titleize
+ %td
+ #{number_with_precision(hook_log.execution_duration, precision: 2)} sec
+ %td
+ = time_ago_with_tooltip(hook_log.created_at)
+ %td
+ = link_to _('View details'), hook_log_path(hook, hook_log)
+
+
+- if hook_logs.present?
+ = paginate hook_logs, theme: 'gitlab'
+- else
+ .gl-text-center.gl-mt-7
+ %h4= _('No webhook events')
+ %p
+ %span.gl-display-block= _('Webhook events will be displayed here.')
+ %span= _('Use the %{strongStart}Test%{strongEnd} option above to create an event.').html_safe % { strongStart: ''.html_safe, strongEnd: ''.html_safe }
+
diff --git a/app/views/shared/hook_logs/_status_label.html.haml b/app/views/shared/hook_logs/_status_label.html.haml
index dfa5ecee44..b930074303 100644
--- a/app/views/shared/hook_logs/_status_label.html.haml
+++ b/app/views/shared/hook_logs/_status_label.html.haml
@@ -1,3 +1,3 @@
- label_status = hook_log.success? ? 'badge-success' : 'badge-danger'
-%span{ class: "badge #{label_status}" }
+%span{ class: "badge badge-pill gl-badge sm #{label_status}" }
= hook_log.internal_error? ? _('Error') : hook_log.response_status
diff --git a/app/views/shared/integrations/_tabs.html.haml b/app/views/shared/integrations/_tabs.html.haml
index 553401e47b..d6ca0bd7d1 100644
--- a/app/views/shared/integrations/_tabs.html.haml
+++ b/app/views/shared/integrations/_tabs.html.haml
@@ -1,18 +1,11 @@
-- active_tab = local_assigns.fetch(:active_tab, 'edit')
-- active_classes = 'gl-tab-nav-item-active gl-tab-nav-item-active-indigo active'
-- tabs = integration_tabs(integration: integration)
-
-- if tabs.length <= 1
- = yield
-- else
+- if integration.instance_level?
.tabs.gl-tabs
%div
- %ul.nav.gl-tabs-nav{ role: 'tablist' }
- - tabs.each do |tab|
- %li.nav-item{ role: 'presentation' }
- %a.nav-link.gl-tab-nav-item{ role: 'tab', class: (active_classes if tab[:key] == active_tab), href: tab[:href] }
- = tab[:text]
+ = gl_tabs_nav({ class: 'gl-mb-5' }) do
+ = gl_tab_link_to _('Settings'), scoped_edit_integration_path(integration)
+ = gl_tab_link_to s_('Integrations|Projects using custom settings'), scoped_overrides_integration_path(integration)
- .tab-content.gl-tab-content
- .tab-pane.gl-pt-3.active{ role: 'tabpanel' }
- = yield
+ = yield
+
+- else
+ = yield
diff --git a/app/views/shared/issuable/_nav.html.haml b/app/views/shared/issuable/_nav.html.haml
index cff50eef88..4a33f62534 100644
--- a/app/views/shared/issuable/_nav.html.haml
+++ b/app/views/shared/issuable/_nav.html.haml
@@ -2,22 +2,16 @@
- page_context_word = type.to_s.humanize(capitalize: false)
- display_count = local_assigns.fetch(:display_count, true)
-%ul.nav-links.issues-state-filters.mobile-separator.nav.nav-tabs
- %li{ class: active_when(params[:state] == 'opened') }>
- = link_to page_filter_path(state: 'opened'), id: 'state-opened', title: _("Filter by %{page_context_word} that are currently open.") % { page_context_word: page_context_word }, data: { state: 'opened' } do
- #{issuables_state_counter_text(type, :opened, display_count)}
-
+= gl_tabs_nav({ class: 'issues-state-filters gl-border-b-0 gl-flex-grow-1' }) do
+ = gl_tab_link_to page_filter_path(state: 'opened'), { item_active: params[:state] == 'opened', id: 'state-opened', title: _("Filter by %{page_context_word} that are currently open.") % { page_context_word: page_context_word }, data: { state: 'opened' } } do
+ #{issuables_state_counter_text(type, :opened, display_count)}
- if type == :merge_requests
- %li{ class: active_when(params[:state] == 'merged') }>
- = link_to page_filter_path(state: 'merged'), id: 'state-merged', title: _('Filter by merge requests that are currently merged.'), data: { state: 'merged' } do
- #{issuables_state_counter_text(type, :merged, display_count)}
-
- %li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
- #{issuables_state_counter_text(type, :closed, display_count)}
+ = gl_tab_link_to page_filter_path(state: 'merged'), item_active: params[:state] == 'merged', id: 'state-merged', title: _('Filter by merge requests that are currently merged.'), data: { state: 'merged' } do
+ #{issuables_state_counter_text(type, :merged, display_count)}
+ = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by merge requests that are currently closed and unmerged.'), data: { state: 'closed' } do
+ #{issuables_state_counter_text(type, :closed, display_count)}
- else
- %li{ class: active_when(params[:state] == 'closed') }>
- = link_to page_filter_path(state: 'closed'), id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
- #{issuables_state_counter_text(type, :closed, display_count)}
+ = gl_tab_link_to page_filter_path(state: 'closed'), item_active: params[:state] == 'closed', id: 'state-closed', title: _('Filter by issues that are currently closed.'), data: { state: 'closed', qa_selector: 'closed_issues_link' } do
+ #{issuables_state_counter_text(type, :closed, display_count)}
= render 'shared/issuable/nav_links/all', page_context_word: page_context_word, counter: issuables_state_counter_text(type, :all, display_count)
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index e6c4b3f481..81a7581d39 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -19,7 +19,7 @@
- if params[:search].present?
= hidden_field_tag :search, params[:search]
- if @can_bulk_update
- .check-all-holder.d-none.d-sm-block.hidden
+ .check-all-holder.gl-display-none.gl-sm-display-block.hidden.gl-float-left.gl-mr-5.gl-line-height-36
- checkbox_id = 'check-all-issues'
%label.gl-sr-only{ for: checkbox_id }= _('Select all')
= check_box_tag checkbox_id, nil, false, class: "check-all-issues left"
diff --git a/app/views/shared/issuable/_sidebar.html.haml b/app/views/shared/issuable/_sidebar.html.haml
index 1e8724c344..62539bfeff 100644
--- a/app/views/shared/issuable/_sidebar.html.haml
+++ b/app/views/shared/issuable/_sidebar.html.haml
@@ -7,6 +7,7 @@
- can_edit_issuable = issuable_sidebar.dig(:current_user, :can_edit)
- add_page_startup_api_call "#{issuable_sidebar[:issuable_json_path]}?serializer=sidebar_extras"
- reviewers = local_assigns.fetch(:reviewers, nil)
+- in_group_context_with_iterations = @project.group.present? && issuable_sidebar[:supports_iterations]
%aside.right-sidebar.js-right-sidebar.js-issuable-sidebar{ data: { signed: { in: signed_in }, issuable_type: issuable_type }, class: sidebar_gutter_collapsed_class, 'aria-live' => 'polite', 'aria-label': issuable_type }
.issuable-sidebar
@@ -28,11 +29,11 @@
= render_if_exists 'shared/issuable/sidebar_item_epic', issuable_sidebar: issuable_sidebar, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:supports_milestone]
- .block.milestone{ :class => ("gl-border-b-0!" if issuable_sidebar[:supports_iterations]), data: { qa_selector: 'milestone_block' } }
+ .block.milestone{ :class => ("gl-border-b-0!" if in_group_context_with_iterations), data: { qa_selector: 'milestone_block' } }
.js-milestone-select{ data: { can_edit: can_edit_issuable.to_s, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid] } }
- - if @project.group.present? && issuable_sidebar[:supports_iterations]
- .block{ class: 'gl-pt-0!', data: { qa_selector: 'iteration_container' } }
+ - if in_group_context_with_iterations
+ .block{ class: 'gl-pt-0! gl-collapse-empty', data: { qa_selector: 'iteration_container', testid: 'iteration_container' } }<
= render_if_exists 'shared/issuable/iteration_select', can_edit: can_edit_issuable.to_s, group_path: @project.group.full_path, project_path: issuable_sidebar[:project_full_path], issue_iid: issuable_sidebar[:iid], issuable_type: issuable_type
- if issuable_sidebar[:supports_time_tracking]
@@ -55,11 +56,13 @@
.js-sidebar-status-entry-point{ data: sidebar_status_data(issuable_sidebar, @project) }
- if issuable_sidebar.has_key?(:confidential)
+ -# haml-lint:disable InlineJavaScript
%script#js-confidential-issue-data{ type: "application/json" }= { is_confidential: issuable_sidebar[:confidential], is_editable: can_edit_issuable }.to_json.html_safe
#js-confidential-entry-point
= render_if_exists 'shared/issuable/sidebar_cve_id_request', issuable_sidebar: issuable_sidebar
+ -# haml-lint:disable InlineJavaScript
%script#js-lock-issue-data{ type: "application/json" }= { is_locked: !!issuable_sidebar[:discussion_locked], is_editable: can_edit_issuable }.to_json.html_safe
#js-lock-entry-point
@@ -72,7 +75,7 @@
#js-reference-entry-point
- if issuable_type == 'merge_request'
.sub-block.js-sidebar-source-branch
- .sidebar-collapsed-icon.dont-change-state
+ .sidebar-collapsed-icon.js-dont-change-state
= clipboard_button(text: source_branch, title: _('Copy branch name'), placement: "left", boundary: 'viewport')
.gl-display-flex.gl-align-items-center.gl-justify-content-space-between.gl-mb-2.hide-collapsed
%span.gl-overflow-hidden.gl-text-overflow-ellipsis.gl-white-space-nowrap
diff --git a/app/views/shared/issuable/nav_links/_all.html.haml b/app/views/shared/issuable/nav_links/_all.html.haml
index c92a50bcb7..7afa194d5d 100644
--- a/app/views/shared/issuable/nav_links/_all.html.haml
+++ b/app/views/shared/issuable/nav_links/_all.html.haml
@@ -1,6 +1,5 @@
- page_context_word = local_assigns.fetch(:page_context_word)
- counter = local_assigns.fetch(:counter)
-%li{ class: active_when(params[:state] == 'all') }>
- = link_to page_filter_path(state: 'all'), id: 'state-all', title: "Show all #{page_context_word}.", data: { state: 'all' } do
- #{counter}
+= gl_tab_link_to page_filter_path(state: 'all'), item_active: params[:state] == 'all', id: 'state-all', title: _("Show all %{issuable_type}.") % { issuable_type: page_context_word }, data: { state: 'all' } do
+ #{counter}
diff --git a/app/views/shared/issue_type/_emoji_block.html.haml b/app/views/shared/issue_type/_emoji_block.html.haml
index 26d3034199..d2c851a4e4 100644
--- a/app/views/shared/issue_type/_emoji_block.html.haml
+++ b/app/views/shared/issue_type/_emoji_block.html.haml
@@ -4,7 +4,7 @@
.row.gl-m-0.gl-justify-content-space-between
.js-noteable-awards
= render 'award_emoji/awards_block', awardable: issuable, inline: true, api_awards_path: api_awards_path
- .new-branch-col.gl-my-2
+ .new-branch-col.gl-my-2.gl-font-size-0
= render_if_exists "projects/issues/timeline_toggle", issuable: issuable
#js-vue-sort-issue-discussions
#js-vue-discussion-filter{ data: { default_filter: current_user&.notes_filter_for(issuable), notes_filters: UserPreference.notes_filters.to_json } }
diff --git a/app/views/shared/milestones/_sidebar.html.haml b/app/views/shared/milestones/_sidebar.html.haml
index 56b2b0d580..c66ba5ba2e 100644
--- a/app/views/shared/milestones/_sidebar.html.haml
+++ b/app/views/shared/milestones/_sidebar.html.haml
@@ -161,11 +161,11 @@
- milestone_ref = milestone.try(:to_reference, full: true)
- if milestone_ref.present?
.block.reference
- .sidebar-collapsed-icon.dont-change-state
+ .sidebar-collapsed-icon.js-dont-change-state
= clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
.cross-project-reference.hide-collapsed
- %span
+ %span.gl-display-inline-block.gl-text-truncate
= s_('MilestoneSidebar|Reference:')
%span{ title: milestone_ref }
= milestone_ref
- = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport')
+ = clipboard_button(text: milestone_ref, title: s_('MilestoneSidebar|Copy reference'), placement: "left", boundary: 'viewport', class: 'btn-clipboard btn-transparent gl-float-right gl-bg-gray-10')
diff --git a/app/views/shared/notes/_comment_button.html.haml b/app/views/shared/notes/_comment_button.html.haml
index d0a2d97df0..3e880a36e2 100644
--- a/app/views/shared/notes/_comment_button.html.haml
+++ b/app/views/shared/notes/_comment_button.html.haml
@@ -1,31 +1,4 @@
- noteable_name = @note.noteable.human_class_name
-.float-left.btn-group.gl-sm-mr-3.droplab-dropdown.comment-type-dropdown.js-comment-type-dropdown
+.js-comment-type-dropdown.float-left.gl-sm-mr-3{ data: { noteable_name: noteable_name } }
%input.btn.gl-button.btn-confirm.js-comment-button.js-comment-submit-button{ type: 'submit', value: _('Comment'), data: { qa_selector: 'comment_button' } }
-
- - if @note.can_be_discussion_note?
- = button_tag type: 'button', class: 'gl-button btn dropdown-toggle btn-confirm js-note-new-discussion js-disable-on-submit', data: { 'dropdown-trigger' => '#resolvable-comment-menu' }, 'aria-label' => _('Open comment type dropdown') do
- = sprite_icon('chevron-down')
-
- %ul#resolvable-comment-menu.dropdown-menu.dropdown-open-top{ data: { dropdown: true } }
- %li#comment.droplab-item-selected{ data: { value: '', 'submit-text' => _('Comment'), 'close-text' => _("Comment & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Comment & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
- %button.btn.gl-button.btn-default-tertiary
- = sprite_icon('check', css_class: 'icon')
- .description
- %strong= _("Comment")
- %p
- = _("Add a general comment to this %{noteable_name}.") % { noteable_name: noteable_name }
-
- %li.divider.droplab-item-ignore
-
- %li#discussion{ data: { value: 'DiscussionNote', 'submit-text' => _('Start thread'), 'close-text' => _("Start thread & close %{noteable_name}") % { noteable_name: noteable_name }, 'reopen-text' => _("Start thread & reopen %{noteable_name}") % { noteable_name: noteable_name } } }
- %button.btn.gl-button.btn-default-tertiary
- = sprite_icon('check', css_class: 'icon')
- .description
- %strong= _("Start thread")
- %p
- = succeed '.' do
- - if @note.noteable.supports_resolvable_notes?
- = _('Discuss a specific suggestion or question that needs to be resolved')
- - else
- = _('Discuss a specific suggestion or question')
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index a03e8446f5..6231f81770 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -1,4 +1,5 @@
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
+- supports_file_upload = local_assigns.fetch(:supports_file_upload, true)
.comment-toolbar.clearfix
.toolbar-text
= link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank'
@@ -10,33 +11,34 @@
is
supported
- %span.uploading-container
- %span.uploading-progress-container.hide
- = sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
- %span.attaching-file-message
- -# Populated by app/assets/javascripts/dropzone_input.js
- %span.uploading-progress 0%
- = loading_icon(css_class: 'align-text-bottom gl-mr-2')
-
- %span.uploading-error-container.hide
- %span.uploading-error-icon
+ - if supports_file_upload
+ %span.uploading-container
+ %span.uploading-progress-container.hide
= sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
- %span.uploading-error-message
- -# Populated by app/assets/javascripts/dropzone_input.js
- %button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
- %span.gl-button-text
- = _("Try again")
- = _("or")
- %button.btn.gl-button.btn-link.attach-new-file.markdown-selector.gl-vertical-align-baseline
- %span.gl-button-text
- = _("attach a new file")
- = _(".")
+ %span.attaching-file-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %span.uploading-progress 0%
+ = loading_icon(css_class: 'align-text-bottom gl-mr-2')
- %button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
- = sprite_icon('media')
- %span.gl-button-text
- = _("Attach a file")
+ %span.uploading-error-container.hide
+ %span.uploading-error-icon
+ = sprite_icon('media', css_class: 'gl-icon gl-vertical-align-text-bottom')
+ %span.uploading-error-message
+ -# Populated by app/assets/javascripts/dropzone_input.js
+ %button.btn.gl-button.btn-link.gl-vertical-align-baseline.retry-uploading-link
+ %span.gl-button-text
+ = _("Try again")
+ = _("or")
+ %button.btn.gl-button.btn-link.attach-new-file.markdown-selector.gl-vertical-align-baseline
+ %span.gl-button-text
+ = _("attach a new file")
+ = _(".")
- %button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
- %span.gl-button-text
- = _("Cancel")
+ %button.btn.gl-button.btn-link.button-attach-file.markdown-selector.button-attach-file.gl-vertical-align-text-bottom
+ = sprite_icon('media')
+ %span.gl-button-text
+ = _("Attach a file")
+
+ %button.btn.gl-button.btn-link.button-cancel-uploading-files.gl-vertical-align-baseline.hide
+ %span.gl-button-text
+ = _("Cancel")
diff --git a/app/views/shared/notes/_notes_with_form.html.haml b/app/views/shared/notes/_notes_with_form.html.haml
index f7f5c02370..e34f412baa 100644
--- a/app/views/shared/notes/_notes_with_form.html.haml
+++ b/app/views/shared/notes/_notes_with_form.html.haml
@@ -25,4 +25,5 @@
= sprite_icon('lock', css_class: 'icon')
%span
= html_escape(_("This %{issuable} is locked. Only %{strong_open}project members%{strong_close} can comment.")) % { issuable: issuable.class.to_s.titleize.downcase, strong_open: ''.html_safe, strong_close: ''.html_safe }
+-# haml-lint:disable InlineJavaScript
%script.js-notes-data{ type: "application/json" }= initial_notes_data(autocomplete).to_json.html_safe
diff --git a/app/views/shared/topics/_search_form.html.haml b/app/views/shared/topics/_search_form.html.haml
new file mode 100644
index 0000000000..97343983b3
--- /dev/null
+++ b/app/views/shared/topics/_search_form.html.haml
@@ -0,0 +1,7 @@
+= form_tag page_filter_path, method: :get, class: "topic-filter-form js-topic-filter-form", id: 'topic-filter-form' do |f|
+ = search_field_tag :search, params[:search],
+ placeholder: s_('Filter by name'),
+ class: 'topic-filter-form-field form-control input-short',
+ spellcheck: false,
+ id: 'topic-filter-form-field',
+ autofocus: local_assigns[:autofocus]
diff --git a/app/views/shared/web_hooks/_hook.html.haml b/app/views/shared/web_hooks/_hook.html.haml
index abe23d0be7..fd124c2967 100644
--- a/app/views/shared/web_hooks/_hook.html.haml
+++ b/app/views/shared/web_hooks/_hook.html.haml
@@ -5,12 +5,12 @@
%div
- hook.class.triggers.each_value do |trigger|
- if hook.public_send(trigger)
- %span.gl-badge.gl-bg-gray-10.gl-mt-2.rounded.deploy-project-label= trigger.to_s.titleize
- %span.gl-badge.gl-bg-gray-10.gl-mt-2.rounded
+ %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2.deploy-project-label= trigger.to_s.titleize
+ %span.gl-badge.badge-muted.badge-pill.sm.gl-mt-2
= _('SSL Verification:')
= hook.enable_ssl_verification ? _('enabled') : _('disabled')
.col-md-4.col-lg-5.text-right-md.gl-mt-2
%span>= render 'shared/web_hooks/test_button', hook: hook, button_class: 'btn-sm btn-default gl-mr-3'
%span>= link_to _('Edit'), edit_hook_path(hook), class: 'btn gl-button btn-default btn-sm gl-mr-3'
- = link_to _('Delete'), destroy_hook_path(hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn gl-button btn-default btn-sm'
+ = link_to _('Delete'), destroy_hook_path(hook), data: { confirm: _('Are you sure?') }, method: :delete, class: 'btn gl-button btn-secondary btn-danger-secondary btn-sm'
diff --git a/app/views/shared/wikis/edit.html.haml b/app/views/shared/wikis/edit.html.haml
index 15710f0df4..e0860bc473 100644
--- a/app/views/shared/wikis/edit.html.haml
+++ b/app/views/shared/wikis/edit.html.haml
@@ -4,7 +4,7 @@
- if @error
#js-wiki-error{ data: { error: @error, wiki_page_path: wiki_page_path(@wiki, @page) } }
-.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
+.js-wiki-edit-page.wiki-page-header.top-area.has-sidebar-toggle.flex-column.flex-lg-row
= wiki_sidebar_toggle_button
%h3.page-title.gl-flex-grow-1
diff --git a/app/views/snippets/show.html.haml b/app/views/snippets/show.html.haml
index ca52a1f8f4..f1093a3b73 100644
--- a/app/views/snippets/show.html.haml
+++ b/app/views/snippets/show.html.haml
@@ -12,7 +12,7 @@
- content_for :prefetch_asset_tags do
- webpack_preload_asset_tag('monaco', prefetch: true)
-#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet) } }
+#js-snippet-view{ data: {'qa-selector': 'snippet_view', 'snippet-gid': @snippet.to_global_id, 'report-abuse-path': snippet_report_abuse_path(@snippet), 'can-report-spam': @snippet.submittable_as_spam_by?(current_user).to_s } }
.row-content-block.top-block.content-component-block
= render 'award_emoji/awards_block', awardable: @snippet, inline: true
diff --git a/app/views/users/show.html.haml b/app/views/users/show.html.haml
index 20cbe08225..522f0f771c 100644
--- a/app/views/users/show.html.haml
+++ b/app/views/users/show.html.haml
@@ -84,10 +84,12 @@
= sprite_icon('location', css_class: 'fgray')
%span{ itemprop: 'addressLocality' }
= @user.location
- = render 'middle_dot_divider', stacking: true do
- = sprite_icon('clock', css_class: 'fgray')
- %span
- = local_time(@user.timezone)
+ - user_local_time = local_time(@user.timezone)
+ - unless user_local_time.nil?
+ = render 'middle_dot_divider', stacking: true, data: { testid: 'user-local-time' } do
+ = sprite_icon('clock', css_class: 'fgray')
+ %span
+ = user_local_time
- unless work_information(@user).blank?
= render 'middle_dot_divider', stacking: true do
= sprite_icon('work', css_class: 'fgray')
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 955674b52a..c7ce2eb8d0 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -30,6 +30,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: authorized_project_update:authorized_project_update_project_recalculate_per_user
+ :worker_name: AuthorizedProjectUpdate::ProjectRecalculatePerUserWorker
+ :feature_category: :authentication_and_authorization
+ :has_external_dependencies:
+ :urgency: :high
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_from_replica
:worker_name: AuthorizedProjectUpdate::UserRefreshFromReplicaWorker
:feature_category: :authentication_and_authorization
@@ -46,7 +55,7 @@
:urgency: :low
:resource_boundary: :unknown
:weight: 1
- :idempotent:
+ :idempotent: true
:tags: []
- :name: authorized_project_update:authorized_project_update_user_refresh_with_low_urgency
:worker_name: AuthorizedProjectUpdate::UserRefreshWithLowUrgencyWorker
@@ -185,7 +194,7 @@
:tags: []
- :name: cronjob:ci_delete_unit_tests
:worker_name: Ci::DeleteUnitTestsWorker
- :feature_category: :continuous_integration
+ :feature_category: :code_testing
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -194,7 +203,7 @@
:tags: []
- :name: cronjob:ci_pipeline_artifacts_expire_artifacts
:worker_name: Ci::PipelineArtifacts::ExpireArtifactsWorker
- :feature_category: :continuous_integration
+ :feature_category: :build_artifacts
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -219,6 +228,24 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:ci_stuck_builds_drop_running
+ :worker_name: Ci::StuckBuilds::DropRunningWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: cronjob:ci_stuck_builds_drop_scheduled
+ :worker_name: Ci::StuckBuilds::DropScheduledWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: cronjob:container_expiration_policy
:worker_name: ContainerExpirationPolicyWorker
:feature_category: :container_registry
@@ -255,6 +282,15 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: cronjob:dependency_proxy_image_ttl_group_policy
+ :worker_name: DependencyProxy::ImageTtlGroupPolicyWorker
+ :feature_category: :dependency_proxy
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent:
+ :tags: []
- :name: cronjob:environments_auto_delete_cron
:worker_name: Environments::AutoDeleteCronWorker
:feature_category: :continuous_delivery
@@ -275,7 +311,7 @@
:tags: []
- :name: cronjob:expire_build_artifacts
:worker_name: ExpireBuildArtifactsWorker
- :feature_category: :continuous_integration
+ :feature_category: :build_artifacts
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -347,7 +383,7 @@
:tags: []
- :name: cronjob:namespaces_in_product_marketing_emails
:worker_name: Namespaces::InProductMarketingEmailsWorker
- :feature_category: :subgroups
+ :feature_category: :experimentation_activation
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -557,7 +593,7 @@
:feature_category: :continuous_integration
:has_external_dependencies:
:urgency: :low
- :resource_boundary: :cpu
+ :resource_boundary: :unknown
:weight: 1
:idempotent:
:tags: []
@@ -642,6 +678,24 @@
:weight: 1
:idempotent: true
:tags: []
+- :name: dependency_proxy_blob:dependency_proxy_cleanup_blob
+ :worker_name: DependencyProxy::CleanupBlobWorker
+ :feature_category: :dependency_proxy
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
+- :name: dependency_proxy_manifest:dependency_proxy_cleanup_manifest
+ :worker_name: DependencyProxy::CleanupManifestWorker
+ :feature_category: :dependency_proxy
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :unknown
+ :weight: 1
+ :idempotent: true
+ :tags: []
- :name: deployment:deployments_drop_older_deployments
:worker_name: Deployments::DropOlderDeploymentsWorker
:feature_category: :continuous_delivery
@@ -1418,7 +1472,7 @@
:urgency: :high
:resource_boundary: :unknown
:weight: 3
- :idempotent:
+ :idempotent: true
:tags: []
- :name: pipeline_cache:expire_pipeline_cache
:worker_name: ExpirePipelineCacheWorker
@@ -1474,6 +1528,15 @@
:weight: 3
:idempotent:
:tags: []
+- :name: pipeline_default:ci_create_downstream_pipeline
+ :worker_name: Ci::CreateDownstreamPipelineWorker
+ :feature_category: :continuous_integration
+ :has_external_dependencies:
+ :urgency: :low
+ :resource_boundary: :cpu
+ :weight: 3
+ :idempotent:
+ :tags: []
- :name: pipeline_default:ci_drop_pipeline
:worker_name: Ci::DropPipelineWorker
:feature_category: :continuous_integration
@@ -1890,7 +1953,7 @@
:tags: []
- :name: default
:worker_name:
- :feature_category:
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency:
:resource_boundary:
@@ -2044,7 +2107,7 @@
:tags: []
- :name: expire_build_instance_artifacts
:worker_name: ExpireBuildInstanceArtifactsWorker
- :feature_category: :continuous_integration
+ :feature_category: :build_artifacts
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2215,7 +2278,7 @@
:tags: []
- :name: mailers
:worker_name: ActionMailer::MailDeliveryJob
- :feature_category: :issue_tracking
+ :feature_category: :not_owned
:has_external_dependencies:
:urgency: low
:resource_boundary:
@@ -2314,7 +2377,7 @@
:tags: []
- :name: namespaces_onboarding_issue_created
:worker_name: Namespaces::OnboardingIssueCreatedWorker
- :feature_category: :issue_tracking
+ :feature_category: :onboarding
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2323,7 +2386,7 @@
:tags: []
- :name: namespaces_onboarding_pipeline_created
:worker_name: Namespaces::OnboardingPipelineCreatedWorker
- :feature_category: :subgroups
+ :feature_category: :onboarding
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2332,7 +2395,7 @@
:tags: []
- :name: namespaces_onboarding_progress
:worker_name: Namespaces::OnboardingProgressWorker
- :feature_category: :product_analytics
+ :feature_category: :onboarding
:has_external_dependencies:
:urgency: :low
:resource_boundary: :cpu
@@ -2341,7 +2404,7 @@
:tags: []
- :name: namespaces_onboarding_user_added
:worker_name: Namespaces::OnboardingUserAddedWorker
- :feature_category: :users
+ :feature_category: :onboarding
:has_external_dependencies:
:urgency: :low
:resource_boundary: :unknown
@@ -2411,15 +2474,6 @@
:weight: 1
:idempotent:
:tags: []
-- :name: pages_remove
- :worker_name: PagesRemoveWorker
- :feature_category: :pages
- :has_external_dependencies:
- :urgency: :low
- :resource_boundary: :unknown
- :weight: 1
- :idempotent:
- :tags: []
- :name: pages_transfer
:worker_name: PagesTransferWorker
:feature_category: :pages
diff --git a/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb
new file mode 100644
index 0000000000..352c82e502
--- /dev/null
+++ b/app/workers/authorized_project_update/project_recalculate_per_user_worker.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+module AuthorizedProjectUpdate
+ class ProjectRecalculatePerUserWorker < ProjectRecalculateWorker
+ data_consistency :always
+
+ feature_category :authentication_and_authorization
+ urgency :high
+ queue_namespace :authorized_project_update
+
+ deduplicate :until_executing, including_scheduled: true
+ idempotent!
+
+ def perform(project_id, user_id)
+ project = Project.find_by_id(project_id)
+ user = User.find_by_id(user_id)
+
+ return unless project && user
+
+ in_lock(lock_key(project), ttl: 10.seconds) do
+ AuthorizedProjectUpdate::ProjectRecalculatePerUserService.new(project, user).execute
+ end
+ end
+ end
+end
diff --git a/app/workers/authorized_project_update/project_recalculate_worker.rb b/app/workers/authorized_project_update/project_recalculate_worker.rb
index 4d350d95e7..3d073f1862 100644
--- a/app/workers/authorized_project_update/project_recalculate_worker.rb
+++ b/app/workers/authorized_project_update/project_recalculate_worker.rb
@@ -26,7 +26,9 @@ module AuthorizedProjectUpdate
private
def lock_key(project)
- "#{self.class.name.underscore}/projects/#{project.id}"
+ # The self.class.name.underscore value is hardcoded here as the prefix, so that the same
+ # lock_key for this superclass will be used by the ProjectRecalculatePerUserWorker subclass.
+ "authorized_project_update/project_recalculate_worker/projects/#{project.id}"
end
end
end
diff --git a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
index 48e3d0837c..daebb23baa 100644
--- a/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_from_replica_worker.rb
@@ -30,8 +30,6 @@ module AuthorizedProjectUpdate
# does not allow us to deduplicate these jobs.
# https://gitlab.com/gitlab-org/gitlab/-/issues/325291
def use_replica_if_available(&block)
- return yield unless ::Gitlab::Database::LoadBalancing.enable?
-
::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&block)
end
diff --git a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
index ab4d9c1342..f532744924 100644
--- a/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
+++ b/app/workers/authorized_project_update/user_refresh_over_user_range_worker.rb
@@ -19,11 +19,10 @@ module AuthorizedProjectUpdate
feature_category :authentication_and_authorization
urgency :low
queue_namespace :authorized_project_update
- # This job will not be deduplicated since it is marked with
- # `data_consistency :delayed` and not `idempotent!`
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/325291
+
deduplicate :until_executing, including_scheduled: true
data_consistency :delayed
+ idempotent!
def perform(start_user_id, end_user_id)
User.where(id: start_user_id..end_user_id).find_each do |user| # rubocop: disable CodeReuse/ActiveRecord
diff --git a/app/workers/bulk_import_worker.rb b/app/workers/bulk_import_worker.rb
index fa255d064c..d560ebcc6e 100644
--- a/app/workers/bulk_import_worker.rb
+++ b/app/workers/bulk_import_worker.rb
@@ -26,7 +26,7 @@ class BulkImportWorker # rubocop:disable Scalability/IdempotentWorker
created_entities.first(next_batch_size).each do |entity|
entity.create_pipeline_trackers!
- BulkImports::ExportRequestWorker.perform_async(entity.id) if entity.group_entity?
+ BulkImports::ExportRequestWorker.perform_async(entity.id)
BulkImports::EntityWorker.perform_async(entity.id)
entity.start!
diff --git a/app/workers/bulk_imports/export_request_worker.rb b/app/workers/bulk_imports/export_request_worker.rb
index d5f7215b08..8bc0acc9b2 100644
--- a/app/workers/bulk_imports/export_request_worker.rb
+++ b/app/workers/bulk_imports/export_request_worker.rb
@@ -10,8 +10,6 @@ module BulkImports
worker_has_external_dependencies!
feature_category :importers
- GROUP_EXPORTED_URL_PATH = "/groups/%s/export_relations"
-
def perform(entity_id)
entity = BulkImports::Entity.find(entity_id)
@@ -21,8 +19,7 @@ module BulkImports
private
def request_export(entity)
- http_client(entity.bulk_import.configuration)
- .post(GROUP_EXPORTED_URL_PATH % entity.encoded_source_full_path)
+ http_client(entity.bulk_import.configuration).post(entity.export_relations_url_path)
end
def http_client(configuration)
diff --git a/app/workers/bulk_imports/pipeline_worker.rb b/app/workers/bulk_imports/pipeline_worker.rb
index 760a309a38..35633b5548 100644
--- a/app/workers/bulk_imports/pipeline_worker.rb
+++ b/app/workers/bulk_imports/pipeline_worker.rb
@@ -16,7 +16,7 @@ module BulkImports
def perform(pipeline_tracker_id, stage, entity_id)
pipeline_tracker = ::BulkImports::Tracker
- .with_status(:created)
+ .with_status(:created, :started)
.find_by_id(pipeline_tracker_id)
if pipeline_tracker.present?
@@ -59,18 +59,35 @@ module BulkImports
pipeline_tracker.pipeline_class.new(context).run
pipeline_tracker.finish!
+ rescue BulkImports::NetworkError => e
+ if e.retriable?(pipeline_tracker)
+ logger.error(
+ worker: self.class.name,
+ entity_id: pipeline_tracker.entity.id,
+ pipeline_name: pipeline_tracker.pipeline_name,
+ message: "Retrying error: #{e.message}"
+ )
+
+ reenqueue(pipeline_tracker, delay: e.retry_delay)
+ else
+ fail_tracker(pipeline_tracker, e)
+ end
rescue StandardError => e
+ fail_tracker(pipeline_tracker, e)
+ end
+
+ def fail_tracker(pipeline_tracker, exception)
pipeline_tracker.update!(status_event: 'fail_op', jid: jid)
logger.error(
worker: self.class.name,
entity_id: pipeline_tracker.entity.id,
pipeline_name: pipeline_tracker.pipeline_name,
- message: e.message
+ message: exception.message
)
Gitlab::ErrorTracking.track_exception(
- e,
+ exception,
entity_id: pipeline_tracker.entity.id,
pipeline_name: pipeline_tracker.pipeline_name
)
@@ -88,8 +105,13 @@ module BulkImports
(Time.zone.now - pipeline_tracker.entity.created_at) > Pipeline::NDJSON_EXPORT_TIMEOUT
end
- def reenqueue(pipeline_tracker)
- self.class.perform_in(NDJSON_PIPELINE_PERFORM_DELAY, pipeline_tracker.id, pipeline_tracker.stage, pipeline_tracker.entity.id)
+ def reenqueue(pipeline_tracker, delay: NDJSON_PIPELINE_PERFORM_DELAY)
+ self.class.perform_in(
+ delay,
+ pipeline_tracker.id,
+ pipeline_tracker.stage,
+ pipeline_tracker.entity.id
+ )
end
end
end
diff --git a/app/workers/ci/build_finished_worker.rb b/app/workers/ci/build_finished_worker.rb
index 3bca301598..f047ba8fde 100644
--- a/app/workers/ci/build_finished_worker.rb
+++ b/app/workers/ci/build_finished_worker.rb
@@ -15,13 +15,13 @@ module Ci
ARCHIVE_TRACES_IN = 2.minutes.freeze
- # rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
- Ci::Build.find_by(id: build_id).try do |build|
- process_build(build)
- end
+ return unless build = Ci::Build.find_by(id: build_id) # rubocop: disable CodeReuse/ActiveRecord
+ return unless build.project
+ return if build.project.pending_delete?
+
+ process_build(build)
end
- # rubocop: enable CodeReuse/ActiveRecord
private
diff --git a/app/workers/ci/create_downstream_pipeline_worker.rb b/app/workers/ci/create_downstream_pipeline_worker.rb
new file mode 100644
index 0000000000..6d4cd2539c
--- /dev/null
+++ b/app/workers/ci/create_downstream_pipeline_worker.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module Ci
+ class CreateDownstreamPipelineWorker # rubocop:disable Scalability/IdempotentWorker
+ include ::ApplicationWorker
+ include ::PipelineQueue
+
+ sidekiq_options retry: 3
+ worker_resource_boundary :cpu
+
+ def perform(bridge_id)
+ ::Ci::Bridge.find_by_id(bridge_id).try do |bridge|
+ ::Ci::CreateDownstreamPipelineService
+ .new(bridge.project, bridge.user)
+ .execute(bridge)
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/delete_unit_tests_worker.rb b/app/workers/ci/delete_unit_tests_worker.rb
index d5bb72ce80..01d909773d 100644
--- a/app/workers/ci/delete_unit_tests_worker.rb
+++ b/app/workers/ci/delete_unit_tests_worker.rb
@@ -10,7 +10,7 @@ module Ci
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
- feature_category :continuous_integration
+ feature_category :code_testing
idempotent!
def perform
diff --git a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
index 2af07cf6f9..cde3d71286 100644
--- a/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
+++ b/app/workers/ci/pipeline_artifacts/expire_artifacts_worker.rb
@@ -14,7 +14,7 @@ module Ci
deduplicate :until_executed, including_scheduled: true
idempotent!
- feature_category :continuous_integration
+ feature_category :build_artifacts
def perform
service = ::Ci::PipelineArtifacts::DestroyAllExpiredService.new
diff --git a/app/workers/ci/stuck_builds/drop_running_worker.rb b/app/workers/ci/stuck_builds/drop_running_worker.rb
new file mode 100644
index 0000000000..db571fdc38
--- /dev/null
+++ b/app/workers/ci/stuck_builds/drop_running_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Ci
+ module StuckBuilds
+ class DropRunningWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ idempotent!
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This is an instance-wide cleanup query, so there's no meaningful
+ # scope to consider this in the context of.
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ data_consistency :always
+
+ feature_category :continuous_integration
+
+ def perform
+ try_obtain_lease do
+ Ci::StuckBuilds::DropRunningService.new.execute
+ end
+ end
+
+ private
+
+ def lease_timeout
+ 30.minutes
+ end
+ end
+ end
+end
diff --git a/app/workers/ci/stuck_builds/drop_scheduled_worker.rb b/app/workers/ci/stuck_builds/drop_scheduled_worker.rb
new file mode 100644
index 0000000000..923841771c
--- /dev/null
+++ b/app/workers/ci/stuck_builds/drop_scheduled_worker.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+module Ci
+ module StuckBuilds
+ class DropScheduledWorker
+ include ApplicationWorker
+ include ExclusiveLeaseGuard
+
+ idempotent!
+
+ # rubocop:disable Scalability/CronWorkerContext
+ # This is an instance-wide cleanup query, so there's no meaningful
+ # scope to consider this in the context of.
+ include CronjobQueue
+ # rubocop:enable Scalability/CronWorkerContext
+
+ data_consistency :always
+
+ feature_category :continuous_integration
+
+ def perform
+ try_obtain_lease do
+ Ci::StuckBuilds::DropScheduledService.new.execute
+ end
+ end
+
+ private
+
+ def lease_timeout
+ 30.minutes
+ end
+ end
+ end
+end
diff --git a/app/workers/cleanup_container_repository_worker.rb b/app/workers/cleanup_container_repository_worker.rb
index 9adc026ced..7274ecf62f 100644
--- a/app/workers/cleanup_container_repository_worker.rb
+++ b/app/workers/cleanup_container_repository_worker.rb
@@ -28,8 +28,8 @@ class CleanupContainerRepositoryWorker
end
result = Projects::ContainerRepository::CleanupTagsService
- .new(project, current_user, params)
- .execute(container_repository)
+ .new(container_repository, current_user, params)
+ .execute
if run_by_container_expiration_policy? && result[:status] == :success
container_repository.reset_expiration_policy_started_at!
diff --git a/app/workers/concerns/dependency_proxy/cleanup_worker.rb b/app/workers/concerns/dependency_proxy/cleanup_worker.rb
new file mode 100644
index 0000000000..b668634f23
--- /dev/null
+++ b/app/workers/concerns/dependency_proxy/cleanup_worker.rb
@@ -0,0 +1,63 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ module CleanupWorker
+ extend ActiveSupport::Concern
+ include Gitlab::Utils::StrongMemoize
+
+ def perform_work
+ return unless artifact
+
+ log_metadata(artifact)
+
+ artifact.destroy!
+ rescue StandardError
+ artifact&.error!
+ end
+
+ def max_running_jobs
+ ::Gitlab::CurrentSettings.dependency_proxy_ttl_group_policy_worker_capacity
+ end
+
+ def remaining_work_count
+ expired_artifacts.limit(max_running_jobs + 1).count
+ end
+
+ private
+
+ def model
+ raise NotImplementedError
+ end
+
+ def log_metadata
+ raise NotImplementedError
+ end
+
+ def log_cleanup_item
+ raise NotImplementedError
+ end
+
+ def artifact
+ strong_memoize(:artifact) do
+ model.transaction do
+ to_delete = next_item
+
+ if to_delete
+ to_delete.processing!
+ log_cleanup_item(to_delete)
+ end
+
+ to_delete
+ end
+ end
+ end
+
+ def expired_artifacts
+ model.expired
+ end
+
+ def next_item
+ expired_artifacts.lock_next_by(:updated_at).first
+ end
+ end
+end
diff --git a/app/workers/concerns/gitlab/github_import/object_importer.rb b/app/workers/concerns/gitlab/github_import/object_importer.rb
index a377b7a200..e1f404b250 100644
--- a/app/workers/concerns/gitlab/github_import/object_importer.rb
+++ b/app/workers/concerns/gitlab/github_import/object_importer.rb
@@ -26,8 +26,7 @@ module Gitlab
object = representation_class.from_json_hash(hash)
# To better express in the logs what object is being imported.
- self.github_id = object.attributes.fetch(:github_id)
-
+ self.github_identifiers = object.github_identifiers
info(project.id, message: 'starting importer')
importer_class.new(object, project, client).execute
@@ -35,10 +34,10 @@ module Gitlab
Gitlab::GithubImport::ObjectCounter.increment(project, object_type, :imported)
info(project.id, message: 'importer finished')
- rescue KeyError => e
+ rescue NoMethodError => e
# This exception will be more useful in development when a new
# Representation is created but the developer forgot to add a
- # `:github_id` field.
+ # `:github_identifiers` field.
Gitlab::Import::ImportFailureService.track(
project_id: project.id,
error_source: importer_class.name,
@@ -72,7 +71,7 @@ module Gitlab
private
- attr_accessor :github_id
+ attr_accessor :github_identifiers
def info(project_id, extra = {})
Logger.info(log_attributes(project_id, extra))
@@ -82,7 +81,7 @@ module Gitlab
extra.merge(
project_id: project_id,
importer: importer_class.name,
- github_id: github_id
+ github_identifiers: github_identifiers
)
end
end
diff --git a/app/workers/concerns/worker_attributes.rb b/app/workers/concerns/worker_attributes.rb
index eebea30655..6f91418e38 100644
--- a/app/workers/concerns/worker_attributes.rb
+++ b/app/workers/concerns/worker_attributes.rb
@@ -46,8 +46,14 @@ module WorkerAttributes
set_class_attribute(:feature_category, :not_owned)
end
+ # Special case: if a worker is not owned, get the feature category
+ # (if present) from the calling context.
def get_feature_category
- get_class_attribute(:feature_category)
+ feature_category = get_class_attribute(:feature_category)
+
+ return feature_category unless feature_category == :not_owned
+
+ Gitlab::ApplicationContext.current_context_attribute('meta.feature_category') || feature_category
end
def feature_category_not_owned?
diff --git a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
index 433ed5e0ea..69f5906f54 100644
--- a/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
+++ b/app/workers/container_expiration_policies/cleanup_container_repository_worker.rb
@@ -21,6 +21,7 @@ module ContainerExpirationPolicies
cleanup_tags_service_original_size
cleanup_tags_service_before_truncate_size
cleanup_tags_service_after_truncate_size
+ cleanup_tags_service_cached_tags_count
cleanup_tags_service_before_delete_size
cleanup_tags_service_deleted_size
].freeze
@@ -147,13 +148,27 @@ module ContainerExpirationPolicies
log_extra_metadata_on_done(field, value)
end
+ log_truncate(result)
+ log_cache_ratio(result)
+ log_extra_metadata_on_done(:running_jobs_count, running_jobs_count)
+ end
+
+ def log_cache_ratio(result)
+ tags_count = result.payload[:cleanup_tags_service_after_truncate_size]
+ cached_tags_count = result.payload[:cleanup_tags_service_cached_tags_count]
+
+ return unless tags_count && cached_tags_count && tags_count != 0
+
+ log_extra_metadata_on_done(:cleanup_tags_service_cache_hit_ratio, cached_tags_count / tags_count.to_f)
+ end
+
+ def log_truncate(result)
before_truncate_size = result.payload[:cleanup_tags_service_before_truncate_size]
after_truncate_size = result.payload[:cleanup_tags_service_after_truncate_size]
truncated = before_truncate_size &&
after_truncate_size &&
before_truncate_size != after_truncate_size
log_extra_metadata_on_done(:cleanup_tags_service_truncated, !!truncated)
- log_extra_metadata_on_done(:running_jobs_count, running_jobs_count)
end
def policy
diff --git a/app/workers/container_expiration_policy_worker.rb b/app/workers/container_expiration_policy_worker.rb
index a791fe5d35..5fcbd74dda 100644
--- a/app/workers/container_expiration_policy_worker.rb
+++ b/app/workers/container_expiration_policy_worker.rb
@@ -45,8 +45,6 @@ class ContainerExpirationPolicyWorker # rubocop:disable Scalability/IdempotentWo
# not perfomed with a delay
# https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63635#note_603771207
def use_replica_if_available(&blk)
- return yield unless ::Gitlab::Database::LoadBalancing.enable?
-
::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&blk)
end
diff --git a/app/workers/create_note_diff_file_worker.rb b/app/workers/create_note_diff_file_worker.rb
index 4bea4fc872..8481fd0a2a 100644
--- a/app/workers/create_note_diff_file_worker.rb
+++ b/app/workers/create_note_diff_file_worker.rb
@@ -10,8 +10,10 @@ class CreateNoteDiffFileWorker # rubocop:disable Scalability/IdempotentWorker
feature_category :code_review
def perform(diff_note_id)
- diff_note = DiffNote.find(diff_note_id)
+ return unless diff_note_id.present?
- diff_note.create_diff_file
+ diff_note = DiffNote.find_by_id(diff_note_id) # rubocop: disable CodeReuse/ActiveRecord
+
+ diff_note&.create_diff_file
end
end
diff --git a/app/workers/database/drop_detached_partitions_worker.rb b/app/workers/database/drop_detached_partitions_worker.rb
index f9c8ce57a3..1e4dc20a0d 100644
--- a/app/workers/database/drop_detached_partitions_worker.rb
+++ b/app/workers/database/drop_detached_partitions_worker.rb
@@ -10,7 +10,7 @@ module Database
idempotent!
def perform
- Gitlab::Database::Partitioning::DetachedPartitionDropper.new.perform
+ Gitlab::Database::Partitioning.drop_detached_partitions
ensure
Gitlab::Database::Partitioning::PartitionMonitoring.new.report_metrics
end
diff --git a/app/workers/dependency_proxy/cleanup_blob_worker.rb b/app/workers/dependency_proxy/cleanup_blob_worker.rb
new file mode 100644
index 0000000000..054bc5854a
--- /dev/null
+++ b/app/workers/dependency_proxy/cleanup_blob_worker.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ class CleanupBlobWorker
+ include ApplicationWorker
+ include LimitedCapacity::Worker
+ include Gitlab::Utils::StrongMemoize
+ include DependencyProxy::CleanupWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+
+ queue_namespace :dependency_proxy_blob
+ feature_category :dependency_proxy
+ urgency :low
+ worker_resource_boundary :unknown
+ idempotent!
+
+ private
+
+ def model
+ DependencyProxy::Blob
+ end
+
+ def log_metadata(blob)
+ log_extra_metadata_on_done(:dependency_proxy_blob_id, blob.id)
+ log_extra_metadata_on_done(:group_id, blob.group_id)
+ end
+
+ def log_cleanup_item(blob)
+ logger.info(
+ structured_payload(
+ group_id: blob.group_id,
+ dependency_proxy_blob_id: blob.id
+ )
+ )
+ end
+ end
+end
diff --git a/app/workers/dependency_proxy/cleanup_manifest_worker.rb b/app/workers/dependency_proxy/cleanup_manifest_worker.rb
new file mode 100644
index 0000000000..1186efa203
--- /dev/null
+++ b/app/workers/dependency_proxy/cleanup_manifest_worker.rb
@@ -0,0 +1,40 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ class CleanupManifestWorker
+ include ApplicationWorker
+ include LimitedCapacity::Worker
+ include Gitlab::Utils::StrongMemoize
+ include DependencyProxy::CleanupWorker
+
+ data_consistency :always
+
+ sidekiq_options retry: 3
+
+ queue_namespace :dependency_proxy_manifest
+ feature_category :dependency_proxy
+ urgency :low
+ worker_resource_boundary :unknown
+ idempotent!
+
+ private
+
+ def model
+ DependencyProxy::Manifest
+ end
+
+ def log_metadata(manifest)
+ log_extra_metadata_on_done(:dependency_proxy_manifest_id, manifest.id)
+ log_extra_metadata_on_done(:group_id, manifest.group_id)
+ end
+
+ def log_cleanup_item(manifest)
+ logger.info(
+ structured_payload(
+ group_id: manifest.group_id,
+ dependency_proxy_manifest_id: manifest.id
+ )
+ )
+ end
+ end
+end
diff --git a/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
new file mode 100644
index 0000000000..fed469e6dc
--- /dev/null
+++ b/app/workers/dependency_proxy/image_ttl_group_policy_worker.rb
@@ -0,0 +1,70 @@
+# frozen_string_literal: true
+
+module DependencyProxy
+ class ImageTtlGroupPolicyWorker # rubocop:disable Scalability/IdempotentWorker
+ include ApplicationWorker
+ include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
+
+ data_consistency :always
+
+ feature_category :dependency_proxy
+
+ UPDATE_BATCH_SIZE = 100
+
+ def perform
+ DependencyProxy::ImageTtlGroupPolicy.enabled.each do |policy|
+ # Technical Debt: change to read_before https://gitlab.com/gitlab-org/gitlab/-/issues/341536
+ qualified_blobs = policy.group.dependency_proxy_blobs.active.updated_before(policy.ttl)
+ qualified_manifests = policy.group.dependency_proxy_manifests.active.updated_before(policy.ttl)
+
+ enqueue_blob_cleanup_job if expire_artifacts(qualified_blobs, DependencyProxy::Blob)
+ enqueue_manifest_cleanup_job if expire_artifacts(qualified_manifests, DependencyProxy::Manifest)
+ end
+
+ log_counts
+ end
+
+ private
+
+ def expire_artifacts(artifacts, model)
+ rows_updated = false
+
+ artifacts.each_batch(of: UPDATE_BATCH_SIZE) do |batch|
+ rows = batch.update_all(status: :expired)
+ rows_updated ||= rows > 0
+ end
+
+ rows_updated
+ end
+
+ def enqueue_blob_cleanup_job
+ DependencyProxy::CleanupBlobWorker.perform_with_capacity
+ end
+
+ def enqueue_manifest_cleanup_job
+ DependencyProxy::CleanupManifestWorker.perform_with_capacity
+ end
+
+ def log_counts
+ use_replica_if_available do
+ expired_blob_count = DependencyProxy::Blob.expired.count
+ expired_manifest_count = DependencyProxy::Manifest.expired.count
+ processing_blob_count = DependencyProxy::Blob.processing.count
+ processing_manifest_count = DependencyProxy::Manifest.processing.count
+ error_blob_count = DependencyProxy::Blob.error.count
+ error_manifest_count = DependencyProxy::Manifest.error.count
+
+ log_extra_metadata_on_done(:expired_dependency_proxy_blob_count, expired_blob_count)
+ log_extra_metadata_on_done(:expired_dependency_proxy_manifest_count, expired_manifest_count)
+ log_extra_metadata_on_done(:processing_dependency_proxy_blob_count, processing_blob_count)
+ log_extra_metadata_on_done(:processing_dependency_proxy_manifest_count, processing_manifest_count)
+ log_extra_metadata_on_done(:error_dependency_proxy_blob_count, error_blob_count)
+ log_extra_metadata_on_done(:error_dependency_proxy_manifest_count, error_manifest_count)
+ end
+ end
+
+ def use_replica_if_available(&block)
+ ::Gitlab::Database::LoadBalancing::Session.current.use_replicas_for_read_queries(&block)
+ end
+ end
+end
diff --git a/app/workers/expire_build_artifacts_worker.rb b/app/workers/expire_build_artifacts_worker.rb
index 65d387f73e..295703cc1c 100644
--- a/app/workers/expire_build_artifacts_worker.rb
+++ b/app/workers/expire_build_artifacts_worker.rb
@@ -10,7 +10,7 @@ class ExpireBuildArtifactsWorker # rubocop:disable Scalability/IdempotentWorker
include CronjobQueue
# rubocop:enable Scalability/CronWorkerContext
- feature_category :continuous_integration
+ feature_category :build_artifacts
def perform
service = Ci::JobArtifacts::DestroyAllExpiredService.new
diff --git a/app/workers/expire_build_instance_artifacts_worker.rb b/app/workers/expire_build_instance_artifacts_worker.rb
index 96378acca0..77b8f59e36 100644
--- a/app/workers/expire_build_instance_artifacts_worker.rb
+++ b/app/workers/expire_build_instance_artifacts_worker.rb
@@ -7,7 +7,7 @@ class ExpireBuildInstanceArtifactsWorker # rubocop:disable Scalability/Idempoten
sidekiq_options retry: 3
- feature_category :continuous_integration
+ feature_category :build_artifacts
# rubocop: disable CodeReuse/ActiveRecord
def perform(build_id)
diff --git a/app/workers/expire_job_cache_worker.rb b/app/workers/expire_job_cache_worker.rb
index 401fe1dc1e..7374f65054 100644
--- a/app/workers/expire_job_cache_worker.rb
+++ b/app/workers/expire_job_cache_worker.rb
@@ -10,11 +10,9 @@ class ExpireJobCacheWorker # rubocop:disable Scalability/IdempotentWorker
queue_namespace :pipeline_cache
urgency :high
- # This worker should be idempotent, but we're switching to data_consistency
- # :sticky and there is an ongoing incompatibility, so it needs to be disabled for
- # now. The following line can be uncommented and this comment removed once
- # https://gitlab.com/gitlab-org/gitlab/-/issues/325291 is resolved.
- # idempotent!
+
+ deduplicate :until_executing, including_scheduled: true
+ idempotent!
# rubocop: disable CodeReuse/ActiveRecord
def perform(job_id)
diff --git a/app/workers/gitlab/github_import/stage/finish_import_worker.rb b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
index 006b79dbff..5197c1e1e8 100644
--- a/app/workers/gitlab/github_import/stage/finish_import_worker.rb
+++ b/app/workers/gitlab/github_import/stage/finish_import_worker.rb
@@ -18,36 +18,28 @@ module Gitlab
# project - An instance of Project.
def import(_, project)
+ @project = project
project.after_import
- report_import_time(project)
+ report_import_time
end
- def report_import_time(project)
- duration = Time.zone.now - project.created_at
+ private
- histogram.observe({ project: project.full_path }, duration)
- counter.increment
+ attr_reader :project
+
+ def report_import_time
+ metrics.track_finished_import
info(
project.id,
message: "GitHub project import finished",
- duration_s: duration.round(2),
+ duration_s: metrics.duration.round(2),
object_counts: ::Gitlab::GithubImport::ObjectCounter.summary(project)
)
end
- def histogram
- @histogram ||= Gitlab::Metrics.histogram(
- :github_importer_total_duration_seconds,
- 'Total time spent importing GitHub projects, in seconds'
- )
- end
-
- def counter
- @counter ||= Gitlab::Metrics.counter(
- :github_importer_imported_projects,
- 'The number of imported GitHub projects'
- )
+ def metrics
+ @metrics ||= Gitlab::Import::Metrics.new(:github_importer, project)
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
index 715c39caf4..cc6a225516 100644
--- a/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_base_data_worker.rb
@@ -31,6 +31,22 @@ module Gitlab
project.import_state.refresh_jid_expiration
ImportPullRequestsWorker.perform_async(project.id)
+ rescue StandardError => e
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: self.class.name,
+ exception: e,
+ fail_import: abort_on_failure,
+ metrics: true
+ )
+
+ raise(e)
+ end
+
+ private
+
+ def abort_on_failure
+ true
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
index d76d36531d..71d0247bae 100644
--- a/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_pull_requests_worker.rb
@@ -27,6 +27,22 @@ module Gitlab
{ waiter.key => waiter.jobs_remaining },
:pull_requests_merged_by
)
+ rescue StandardError => e
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: self.class.name,
+ exception: e,
+ fail_import: abort_on_failure,
+ metrics: true
+ )
+
+ raise(e)
+ end
+
+ private
+
+ def abort_on_failure
+ true
end
end
end
diff --git a/app/workers/gitlab/github_import/stage/import_repository_worker.rb b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
index 227b7c304b..3e914cc759 100644
--- a/app/workers/gitlab/github_import/stage/import_repository_worker.rb
+++ b/app/workers/gitlab/github_import/stage/import_repository_worker.rb
@@ -33,6 +33,17 @@ module Gitlab
counter.increment
ImportBaseDataWorker.perform_async(project.id)
+
+ rescue StandardError => e
+ Gitlab::Import::ImportFailureService.track(
+ project_id: project.id,
+ error_source: self.class.name,
+ exception: e,
+ fail_import: abort_on_failure,
+ metrics: true
+ )
+
+ raise(e)
end
def counter
diff --git a/app/workers/issue_placement_worker.rb b/app/workers/issue_placement_worker.rb
index e0c4502ed1..22e2a8e95f 100644
--- a/app/workers/issue_placement_worker.rb
+++ b/app/workers/issue_placement_worker.rb
@@ -31,7 +31,7 @@ class IssuePlacementWorker
# while preserving creation order.
to_place = Issue
.relative_positioning_query_base(issue)
- .where(relative_position: nil)
+ .with_null_relative_position
.order({ created_at: :asc }, { id: :asc })
.limit(QUERY_LIMIT + 1)
.to_a
diff --git a/app/workers/namespaces/in_product_marketing_emails_worker.rb b/app/workers/namespaces/in_product_marketing_emails_worker.rb
index 49e65d59e8..470fba1227 100644
--- a/app/workers/namespaces/in_product_marketing_emails_worker.rb
+++ b/app/workers/namespaces/in_product_marketing_emails_worker.rb
@@ -8,7 +8,7 @@ module Namespaces
include CronjobQueue # rubocop:disable Scalability/CronWorkerContext
- feature_category :subgroups
+ feature_category :experimentation_activation
urgency :low
def perform
diff --git a/app/workers/namespaces/onboarding_issue_created_worker.rb b/app/workers/namespaces/onboarding_issue_created_worker.rb
index 81d105ab19..aab5767e0f 100644
--- a/app/workers/namespaces/onboarding_issue_created_worker.rb
+++ b/app/workers/namespaces/onboarding_issue_created_worker.rb
@@ -8,7 +8,7 @@ module Namespaces
sidekiq_options retry: 3
- feature_category :issue_tracking
+ feature_category :onboarding
urgency :low
deduplicate :until_executing
diff --git a/app/workers/namespaces/onboarding_pipeline_created_worker.rb b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
index f9a6b73458..4172e28647 100644
--- a/app/workers/namespaces/onboarding_pipeline_created_worker.rb
+++ b/app/workers/namespaces/onboarding_pipeline_created_worker.rb
@@ -8,7 +8,7 @@ module Namespaces
sidekiq_options retry: 3
- feature_category :subgroups
+ feature_category :onboarding
urgency :low
deduplicate :until_executing
diff --git a/app/workers/namespaces/onboarding_progress_worker.rb b/app/workers/namespaces/onboarding_progress_worker.rb
index b77db1aec5..77a31d85a9 100644
--- a/app/workers/namespaces/onboarding_progress_worker.rb
+++ b/app/workers/namespaces/onboarding_progress_worker.rb
@@ -8,7 +8,7 @@ module Namespaces
sidekiq_options retry: 3
- feature_category :product_analytics
+ feature_category :onboarding
worker_resource_boundary :cpu
urgency :low
diff --git a/app/workers/namespaces/onboarding_user_added_worker.rb b/app/workers/namespaces/onboarding_user_added_worker.rb
index 6a189e81b9..4d17cf9a6e 100644
--- a/app/workers/namespaces/onboarding_user_added_worker.rb
+++ b/app/workers/namespaces/onboarding_user_added_worker.rb
@@ -8,7 +8,7 @@ module Namespaces
sidekiq_options retry: 3
- feature_category :users
+ feature_category :onboarding
urgency :low
idempotent!
diff --git a/app/workers/packages/composer/cache_cleanup_worker.rb b/app/workers/packages/composer/cache_cleanup_worker.rb
index 19babf6396..c80d6ea45d 100644
--- a/app/workers/packages/composer/cache_cleanup_worker.rb
+++ b/app/workers/packages/composer/cache_cleanup_worker.rb
@@ -14,19 +14,7 @@ module Packages
idempotent!
def perform
- ::Packages::Composer::CacheFile.without_namespace.find_in_batches do |cache_files|
- cache_files.each(&:destroy)
- rescue ActiveRecord::RecordNotFound
- # ignore. likely due to object already being deleted.
- end
-
- ::Packages::Composer::CacheFile.expired.find_in_batches do |cache_files|
- cache_files.each(&:destroy)
- rescue ActiveRecord::RecordNotFound
- # ignore. likely due to object already being deleted.
- end
- rescue StandardError => e
- Gitlab::ErrorTracking.log_exception(e)
+ # no-op: to be removed after 14.5 https://gitlab.com/gitlab-org/gitlab/-/issues/333694
end
end
end
diff --git a/app/workers/packages/composer/cache_update_worker.rb b/app/workers/packages/composer/cache_update_worker.rb
index 874993a132..5600af6ce2 100644
--- a/app/workers/packages/composer/cache_update_worker.rb
+++ b/app/workers/packages/composer/cache_update_worker.rb
@@ -7,20 +7,14 @@ module Packages
data_consistency :always
- sidekiq_options retry: 3
+ sidekiq_options retry: false
feature_category :package_registry
idempotent!
- def perform(project_id, package_name, last_page_sha)
- project = Project.find_by_id(project_id)
-
- return unless project
-
- Gitlab::Composer::Cache.new(project: project, name: package_name, last_page_sha: last_page_sha).execute
- rescue StandardError => e
- Gitlab::ErrorTracking.log_exception(e, project_id: project_id)
+ def perform(*args)
+ # no-op: to be removed after 14.5 https://gitlab.com/gitlab-org/gitlab/-/issues/333694
end
end
end
diff --git a/app/workers/packages/debian/generate_distribution_worker.rb b/app/workers/packages/debian/generate_distribution_worker.rb
index b9b157d25d..1eff3ea02d 100644
--- a/app/workers/packages/debian/generate_distribution_worker.rb
+++ b/app/workers/packages/debian/generate_distribution_worker.rb
@@ -2,7 +2,7 @@
module Packages
module Debian
- class GenerateDistributionWorker # rubocop:disable Scalability/IdempotentWorker
+ class GenerateDistributionWorker
include ApplicationWorker
data_consistency :always
diff --git a/app/workers/pages_remove_worker.rb b/app/workers/pages_remove_worker.rb
deleted file mode 100644
index 4de99b8654..0000000000
--- a/app/workers/pages_remove_worker.rb
+++ /dev/null
@@ -1,16 +0,0 @@
-# frozen_string_literal: true
-
-# TODO: remove this worker https://gitlab.com/gitlab-org/gitlab/-/issues/340641
-class PagesRemoveWorker # rubocop:disable Scalability/IdempotentWorker
- include ApplicationWorker
-
- data_consistency :always
-
- sidekiq_options retry: 3
- feature_category :pages
- loggable_arguments 0
-
- def perform(project_id)
- # no-op
- end
-end
diff --git a/app/workers/pipeline_hooks_worker.rb b/app/workers/pipeline_hooks_worker.rb
index 322f92d376..c67f3860a5 100644
--- a/app/workers/pipeline_hooks_worker.rb
+++ b/app/workers/pipeline_hooks_worker.rb
@@ -12,9 +12,10 @@ class PipelineHooksWorker # rubocop:disable Scalability/IdempotentWorker
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
- Ci::Pipeline
- .find_by(id: pipeline_id)
- .try(:execute_hooks)
+ pipeline = Ci::Pipeline.find_by(id: pipeline_id)
+ return unless pipeline
+
+ Ci::Pipelines::HookService.new(pipeline).execute
end
# rubocop: enable CodeReuse/ActiveRecord
end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index 9cd471a5ab..9370b36106 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -14,7 +14,7 @@ class PipelineProcessWorker
loggable_arguments 1
idempotent!
- deduplicate :until_executing, feature_flag: :ci_idempotent_pipeline_process_worker
+ deduplicate :until_executing
# rubocop: disable CodeReuse/ActiveRecord
def perform(pipeline_id)
diff --git a/app/workers/run_pipeline_schedule_worker.rb b/app/workers/run_pipeline_schedule_worker.rb
index dd0f14a5ca..12042ebc4f 100644
--- a/app/workers/run_pipeline_schedule_worker.rb
+++ b/app/workers/run_pipeline_schedule_worker.rb
@@ -27,8 +27,9 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
user,
ref: schedule.ref)
.execute!(:schedule, ignore_skip_ci: true, save_on_errors: false, schedule: schedule)
- rescue Ci::CreatePipelineService::CreateError
- # no-op. This is a user operation error such as corrupted .gitlab-ci.yml.
+ rescue Ci::CreatePipelineService::CreateError => e
+ # This is a user operation error such as corrupted .gitlab-ci.yml. Log the error for debugging purpose.
+ log_extra_metadata_on_done(:pipeline_creation_error, e)
rescue StandardError => e
error(schedule, e)
end
@@ -37,10 +38,16 @@ class RunPipelineScheduleWorker # rubocop:disable Scalability/IdempotentWorker
def error(schedule, error)
failed_creation_counter.increment
+ log_error(schedule, error)
+ track_error(schedule, error)
+ end
+ def log_error(schedule, error)
Gitlab::AppLogger.error "Failed to create a scheduled pipeline. " \
"schedule_id: #{schedule.id} message: #{error.message}"
+ end
+ def track_error(schedule, error)
Gitlab::ErrorTracking
.track_and_raise_for_dev_exception(error,
issue_url: 'https://gitlab.com/gitlab-org/gitlab-foss/issues/41231',
diff --git a/app/workers/stuck_ci_jobs_worker.rb b/app/workers/stuck_ci_jobs_worker.rb
index a2b2686c8d..72004f7568 100644
--- a/app/workers/stuck_ci_jobs_worker.rb
+++ b/app/workers/stuck_ci_jobs_worker.rb
@@ -2,6 +2,7 @@
class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
include ApplicationWorker
+ include ExclusiveLeaseGuard
# rubocop:disable Scalability/CronWorkerContext
# This is an instance-wide cleanup query, so there's no meaningful
@@ -12,25 +13,19 @@ class StuckCiJobsWorker # rubocop:disable Scalability/IdempotentWorker
data_consistency :always
feature_category :continuous_integration
- worker_resource_boundary :cpu
-
- EXCLUSIVE_LEASE_KEY = 'stuck_ci_builds_worker_lease'
def perform
- return unless try_obtain_lease
+ Ci::StuckBuilds::DropRunningWorker.perform_in(20.minutes)
+ Ci::StuckBuilds::DropScheduledWorker.perform_in(40.minutes)
- Ci::StuckBuilds::DropService.new.execute
-
- remove_lease
+ try_obtain_lease do
+ Ci::StuckBuilds::DropPendingService.new.execute
+ end
end
private
- def try_obtain_lease
- @uuid = Gitlab::ExclusiveLease.new(EXCLUSIVE_LEASE_KEY, timeout: 30.minutes).try_obtain
- end
-
- def remove_lease
- Gitlab::ExclusiveLease.cancel(EXCLUSIVE_LEASE_KEY, @uuid)
+ def lease_timeout
+ 30.minutes
end
end
diff --git a/bin/background_jobs b/bin/background_jobs
index 6aebc8126c..f9b42b97e0 100755
--- a/bin/background_jobs
+++ b/bin/background_jobs
@@ -3,6 +3,7 @@
cd $(dirname $0)/..
app_root=$(pwd)
sidekiq_workers=${SIDEKIQ_WORKERS:-1}
+sidekiq_queues=${SIDEKIQ_QUEUES:-*} # Queues to listen to; default to `*` (all)
sidekiq_pidfile="$app_root/tmp/pids/sidekiq-cluster.pid"
sidekiq_logfile="$app_root/log/sidekiq.log"
gitlab_user=$(ls -l config.ru | awk '{print $3}')
@@ -37,8 +38,7 @@ restart()
stop
fi
- warn "Sidekiq output will be written to $sidekiq_logfile"
- start_sidekiq "$@" >> $sidekiq_logfile 2>&1
+ start_sidekiq "$@"
}
start_sidekiq()
@@ -50,13 +50,13 @@ start_sidekiq()
cmd="${cmd} ${chpst} -P"
fi
- # sidekiq-cluster expects '*' '*' arguments (one wildcard for each process).
+ # sidekiq-cluster expects an argument per process.
for (( i=1; i<=$sidekiq_workers; i++ ))
do
- processes_args+=("*")
+ processes_args+=("${sidekiq_queues}")
done
- ${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@"
+ ${cmd} bin/sidekiq-cluster "${processes_args[@]}" -P $sidekiq_pidfile -e $RAILS_ENV "$@" 2>&1 | tee -a $sidekiq_logfile
}
action="$1"
diff --git a/config/README.md b/config/README.md
index be5bd442fd..f04758fcae 100644
--- a/config/README.md
+++ b/config/README.md
@@ -1,13 +1,13 @@
# Configuration files Documentation
Note that most configuration files (`config/*.*`) committed into
-[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-foss) **will not be used** for
+[gitlab-foss](https://gitlab.com/gitlab-org/gitlab-foss) **will not be used** for
[omnibus-gitlab](https://gitlab.com/gitlab-org/omnibus-gitlab). Configuration
-files committed into gitlab-ce are only used for development.
+files committed into gitlab-foss are only used for development.
## gitlab.yml
-You can find most of GitLab configuration settings here.
+You can find most of the GitLab configuration settings here.
## mail_room.yml
@@ -21,7 +21,7 @@ This file is called `resque.yml` for historical reasons. We are **NOT**
using Resque at the moment. It is used to specify Redis configuration
values when a single database instance of Redis is desired.
-# Advanced Redis configuration files
+## Advanced Redis configuration files
In more advanced configurations of Redis key-value storage, it is desirable
to separate the keys by lifecycle and intended use to ease provisioning and
@@ -40,7 +40,7 @@ If desired, the routing URL provided by these settings can be used with:
2. TCP port number for each Redis instance desired
3. `database number` for each Redis instance desired
-## Example URL attribute formats for GitLab Redis `.yml` configuration files
+### Example URL attribute formats for GitLab Redis `.yml` configuration files
* Unix Socket, default Redis database (0)
* `url: unix:/path/to/redis.sock`
* `url: unix:/path/to/redis.sock?db=`
@@ -52,129 +52,38 @@ If desired, the routing URL provided by these settings can be used with:
* TCP Socket for Redis on remote host `myserver`, port 6379, database 33
* `url: redis://:mynewpassword@myserver:6379/33`
-## redis.cache.yml
+## Available configuration files
-If configured, `redis.cache.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for `Rails.cache` and other volatile non-persistent data which enhances
-the performance of GitLab.
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_CACHE_CONFIG_FILE` which provides
-an alternate location for configuration settings.
+The Redis instances that can be configured are described in the table below. The
+order of precedence for configuration is described below, where `$NAME` and
+`$FALLBACK_NAME` are the upper-cased instance names from the table, and `$name`
+and `$fallback_name` are the lower-cased versions:
-The order of precedence for the URL used to connect to the Redis instance
-used for `cache` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CACHE_CONFIG_FILE` environment variable
-2. URL from `redis.cache.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6380`
+1. The configuration file pointed to by the `GITLAB_REDIS_$NAME_CONFIG_FILE`
+ environment variable.
+1. The configuration file `redis.$name.yml`.
+1. **If a fallback instance is available**, the configuration file
+ `redis.$fallback_name.yml`.
+1. The configuration file pointed to by the `GITLAB_REDIS_CONFIG_FILE`
+environment variable.
+1. The configuration file `resque.yml`.
-The order of precedence for all other configuration settings for `cache`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_CACHE_CONFIG_FILE` environment variable
-2. the configuration file `redis.cache.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
+An example configuration file for Redis is in this directory under the name
+`resque.yml.example`.
-## redis.queues.yml
+| Name | Fallback instance | Purpose |
+| --- | --- | --- |
+| `cache` | | Volatile non-persistent data |
+| `queues` | | Background job processing queues |
+| `shared_state` | | Persistent application state |
+| `trace_chunks` | `shared_state` | [CI trace chunks](https://docs.gitlab.com/ee/administration/job_logs.html#incremental-logging-architecture) |
+| `rate_limiting` | `cache` | [Rate limiting](https://docs.gitlab.com/ee/user/admin_area/settings/user_and_ip_rate_limits.html) state |
+| `sessions` | `shared_state` | [Sessions](https://docs.gitlab.com/ee/development/session.html#redis)|
-If configured, `redis.queues.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::Queues`.
-These queues are intended to be the foundation
-of reliable inter-process communication between modules, whether on the same
-host node, or within a cluster. The primary clients of the queues are
-SideKiq, Mailroom, CI Runner, Workhorse, and push services. Settings here can
-be overridden by the environment variable
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` which provides an alternate location for
-configuration settings.
+If no configuration is found, or no URL is found in the configuration
+file, the default URL used is:
-The order of precedence for the URL used to connect to the Redis instance
-used for `queues` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` environment variable
-2. URL from `redis.queues.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6381`
-
-The order of precedence for all other configuration settings for `queues`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_QUEUES_CONFIG_FILE` environment variable
-2. the configuration file `redis.queues.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
-
-## redis.shared_state.yml
-
-If configured, `redis.shared_state.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::SharedState` such as session state,
-and rate limiting.
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` which provides
-an alternate location for configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `shared_state` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` environment variable
-2. URL from `redis.shared_state.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6382`
-
-The order of precedence for all other configuration settings for `shared_state`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_SHARED_STATE_CONFIG_FILE` environment variable
-2. the configuration file `redis.shared_state.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
-
-## redis.trace_chunks.yml
-
-If configured, `redis.trace_chunks.yml` overrides the
-`resque.yml` settings to configure the Redis database instance
-used for clients of `::Gitlab::Redis::TraceChunks` which stores CI trace chunks.
-
-Settings here can be overridden by the environment variable
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` which provides
-an alternate location for configuration settings.
-
-The order of precedence for the URL used to connect to the Redis instance
-used for `trace_chunks` is:
-1. URL from a configuration file pointed to by the
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` environment variable
-2. URL from `redis.trace_chunks.yml`
-3. URL from a configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. URL from `resque.yml`
-5. `redis://localhost:6383`
-
-The order of precedence for all other configuration settings for `trace_chunks`
-are selected from only the first of the following files found (if a setting
-is not provided in an earlier file, the remainder of the files are not
-searched):
-1. the configuration file pointed to by the
-`GITLAB_REDIS_TRACE_CHUNKS_CONFIG_FILE` environment variable
-2. the configuration file `redis.trace_chunks.yml`
-3. the configuration file pointed to by the
-`GITLAB_REDIS_CONFIG_FILE` environment variable
-4. the configuration file `resque.yml`
+1. `redis://localhost:6380` for `cache`.
+1. `redis://localhost:6381` for `queues`.
+1. `redis://localhost:6382` for `shared_state`.
+1. The URL from the fallback instance for all other instances.
diff --git a/config/application.rb b/config/application.rb
index 2349de4892..dba9550a3d 100644
--- a/config/application.rb
+++ b/config/application.rb
@@ -23,6 +23,9 @@ module Gitlab
require_dependency Rails.root.join('lib/gitlab/redis/cache')
require_dependency Rails.root.join('lib/gitlab/redis/queues')
require_dependency Rails.root.join('lib/gitlab/redis/shared_state')
+ require_dependency Rails.root.join('lib/gitlab/redis/trace_chunks')
+ require_dependency Rails.root.join('lib/gitlab/redis/rate_limiting')
+ require_dependency Rails.root.join('lib/gitlab/redis/sessions')
require_dependency Rails.root.join('lib/gitlab/current_settings')
require_dependency Rails.root.join('lib/gitlab/middleware/read_only')
require_dependency Rails.root.join('lib/gitlab/middleware/basic_health_check')
@@ -370,15 +373,7 @@ module Gitlab
end
# Use caching across all environments
- # Full list of options:
- # https://api.rubyonrails.org/classes/ActiveSupport/Cache/RedisCacheStore.html#method-c-new
- caching_config_hash = {}
- caching_config_hash[:redis] = Gitlab::Redis::Cache.pool
- caching_config_hash[:compress] = Gitlab::Utils.to_boolean(ENV.fetch('ENABLE_REDIS_CACHE_COMPRESSION', '1'))
- caching_config_hash[:namespace] = Gitlab::Redis::Cache::CACHE_NAMESPACE
- caching_config_hash[:expires_in] = 2.weeks # Cache should not grow forever
-
- config.cache_store = :redis_cache_store, caching_config_hash
+ config.cache_store = :redis_cache_store, Gitlab::Redis::Cache.active_support_config
config.active_job.queue_adapter = :sidekiq
diff --git a/config/feature_categories.yml b/config/feature_categories.yml
index 1ecf217dd9..e61048a642 100644
--- a/config/feature_categories.yml
+++ b/config/feature_categories.yml
@@ -33,6 +33,7 @@
- continuous_integration_scaling
- database
- dataops
+- delivery
- delivery_management
- dependency_firewall
- dependency_proxy
@@ -53,16 +54,15 @@
- five_minute_production_app
- foundations
- fuzz_testing
-- gdk
- geo_replication
- git_lfs
- gitaly
- gitlab_docs
- global_search
- helm_chart_registry
+- horse
- importers
- incident_management
-- infrastructure
- infrastructure_as_code
- insider_threat
- integrations
@@ -103,12 +103,12 @@
- roadmaps
- runbooks
- runner
+- scalability
- secret_detection
- secrets_management
- security_benchmarking
- security_orchestration
- self_monitoring
-- serverless
- service_desk
- service_ping
- sharding
diff --git a/config/feature_flags/development/add_actor_based_user_to_snowplow_tracking.yml b/config/feature_flags/development/add_actor_based_user_to_snowplow_tracking.yml
new file mode 100644
index 0000000000..9dc20148d5
--- /dev/null
+++ b/config/feature_flags/development/add_actor_based_user_to_snowplow_tracking.yml
@@ -0,0 +1,8 @@
+---
+name: add_actor_based_user_to_snowplow_tracking
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71353
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338150
+milestone: '14.4'
+type: development
+group: group::product intelligence
+default_enabled: false
diff --git a/config/feature_flags/development/advanced_search_multi_project_select.yml b/config/feature_flags/development/advanced_search_multi_project_select.yml
index 4f38955fa7..8f74c8990f 100644
--- a/config/feature_flags/development/advanced_search_multi_project_select.yml
+++ b/config/feature_flags/development/advanced_search_multi_project_select.yml
@@ -2,7 +2,7 @@
name: advanced_search_multi_project_select
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62606
rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/333011
-milestone: '14.0'
+milestone: '14.4'
type: development
group: group::global search
default_enabled: false
diff --git a/config/feature_flags/development/avoid_cross_joins_environments_in_self_and_descendants.yml b/config/feature_flags/development/avoid_cross_joins_environments_in_self_and_descendants.yml
new file mode 100644
index 0000000000..25b714b2c6
--- /dev/null
+++ b/config/feature_flags/development/avoid_cross_joins_environments_in_self_and_descendants.yml
@@ -0,0 +1,8 @@
+---
+name: avoid_cross_joins_environments_in_self_and_descendants
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71894
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342991
+milestone: '14.4'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/ci_archived_build_trace_checksum.yml b/config/feature_flags/development/ci_archived_build_trace_checksum.yml
new file mode 100644
index 0000000000..95e641e0ef
--- /dev/null
+++ b/config/feature_flags/development/ci_archived_build_trace_checksum.yml
@@ -0,0 +1,8 @@
+---
+name: ci_archived_build_trace_checksum
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70072
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340737
+milestone: '14.4'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/ci_create_external_pr_pipeline_async.yml b/config/feature_flags/development/ci_create_external_pr_pipeline_async.yml
index 3935a818b1..48c7dbcf74 100644
--- a/config/feature_flags/development/ci_create_external_pr_pipeline_async.yml
+++ b/config/feature_flags/development/ci_create_external_pr_pipeline_async.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338908
milestone: '14.3'
type: development
group: group::pipeline authoring
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/ci_idempotent_pipeline_process_worker.yml b/config/feature_flags/development/ci_idempotent_pipeline_process_worker.yml
deleted file mode 100644
index 60104bd310..0000000000
--- a/config/feature_flags/development/ci_idempotent_pipeline_process_worker.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_idempotent_pipeline_process_worker
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/62410
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332963
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/ci_include_rules.yml b/config/feature_flags/development/ci_include_rules.yml
deleted file mode 100644
index d8a3f0b245..0000000000
--- a/config/feature_flags/development/ci_include_rules.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_include_rules
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67409
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337507
-milestone: '14.2'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/ci_minutes_track_live_consumption.yml b/config/feature_flags/development/ci_minutes_track_live_consumption.yml
deleted file mode 100644
index d94dfc4120..0000000000
--- a/config/feature_flags/development/ci_minutes_track_live_consumption.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_minutes_track_live_consumption
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/59263
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329197
-milestone: '13.12'
-type: development
-group: group::pipeline execution
-default_enabled: false
diff --git a/config/feature_flags/development/ci_new_query_for_running_stuck_jobs.yml b/config/feature_flags/development/ci_new_query_for_running_stuck_jobs.yml
new file mode 100644
index 0000000000..345e9b4c3a
--- /dev/null
+++ b/config/feature_flags/development/ci_new_query_for_running_stuck_jobs.yml
@@ -0,0 +1,8 @@
+---
+name: ci_new_query_for_running_stuck_jobs
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71013
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339264
+milestone: '14.4'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/ci_optimize_project_records_destruction.yml b/config/feature_flags/development/ci_optimize_project_records_destruction.yml
new file mode 100644
index 0000000000..73ad4ae995
--- /dev/null
+++ b/config/feature_flags/development/ci_optimize_project_records_destruction.yml
@@ -0,0 +1,8 @@
+---
+name: ci_optimize_project_records_destruction
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71342
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341936
+milestone: '14.4'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/ci_pipeline_add_job_with_lock.yml b/config/feature_flags/development/ci_pipeline_add_job_with_lock.yml
deleted file mode 100644
index 6a708013ca..0000000000
--- a/config/feature_flags/development/ci_pipeline_add_job_with_lock.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_pipeline_add_job_with_lock
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/65754
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337628
-milestone: '14.2'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml b/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml
deleted file mode 100644
index 932ee76634..0000000000
--- a/config/feature_flags/development/ci_remove_update_retried_from_process_pipeline.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_remove_update_retried_from_process_pipeline
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54300
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321630
-milestone: '13.9'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/ci_runner_limits.yml b/config/feature_flags/development/ci_runner_limits.yml
deleted file mode 100644
index e7d30dd086..0000000000
--- a/config/feature_flags/development/ci_runner_limits.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ci_runner_limits
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60157
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329438
-milestone: '13.12'
-type: development
-group: group::runner
-default_enabled: false
diff --git a/config/feature_flags/development/ci_scoped_job_token.yml b/config/feature_flags/development/ci_scoped_job_token.yml
index a7fa024483..a885a1e639 100644
--- a/config/feature_flags/development/ci_scoped_job_token.yml
+++ b/config/feature_flags/development/ci_scoped_job_token.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/332272
milestone: '14.0'
type: development
group: group::pipeline execution
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/container_registry_expiration_policies_caching.yml b/config/feature_flags/development/container_registry_expiration_policies_caching.yml
new file mode 100644
index 0000000000..6e8b0efe94
--- /dev/null
+++ b/config/feature_flags/development/container_registry_expiration_policies_caching.yml
@@ -0,0 +1,8 @@
+---
+name: container_registry_expiration_policies_caching
+introduced_by_url:
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340606
+milestone: '14.3'
+type: development
+group: group::package
+default_enabled: false
diff --git a/config/feature_flags/development/create_vulnerabilities_via_api.yml b/config/feature_flags/development/create_vulnerabilities_via_api.yml
index 0a3f9fa73f..3f8af065dc 100644
--- a/config/feature_flags/development/create_vulnerabilities_via_api.yml
+++ b/config/feature_flags/development/create_vulnerabilities_via_api.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338694
milestone: '14.3'
type: development
group: group::threat insights
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/decomposed_ci_query_in_pipelines_for_merge_request_finder.yml b/config/feature_flags/development/decomposed_ci_query_in_pipelines_for_merge_request_finder.yml
new file mode 100644
index 0000000000..235b37dfb1
--- /dev/null
+++ b/config/feature_flags/development/decomposed_ci_query_in_pipelines_for_merge_request_finder.yml
@@ -0,0 +1,8 @@
+---
+name: decomposed_ci_query_in_pipelines_for_merge_request_finder
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68549
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341341
+milestone: '14.4'
+type: development
+group: group::pipeline execution
+default_enabled: false
diff --git a/config/feature_flags/development/dependency_proxy_workhorse.yml b/config/feature_flags/development/dependency_proxy_workhorse.yml
new file mode 100644
index 0000000000..a3545d32cd
--- /dev/null
+++ b/config/feature_flags/development/dependency_proxy_workhorse.yml
@@ -0,0 +1,8 @@
+---
+name: dependency_proxy_workhorse
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68157
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339639
+milestone: '14.3'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml b/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml
deleted file mode 100644
index 7a52486d35..0000000000
--- a/config/feature_flags/development/ensure_verified_primary_email_for_2fa.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: ensure_verified_primary_email_for_2fa
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69593
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340151
-milestone: '14.3'
-type: development
-group: group::access
-default_enabled: true
diff --git a/config/feature_flags/development/environment_last_visible_pipeline_disable_joins.yml b/config/feature_flags/development/environment_last_visible_pipeline_disable_joins.yml
deleted file mode 100644
index 7667542506..0000000000
--- a/config/feature_flags/development/environment_last_visible_pipeline_disable_joins.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: environment_last_visible_pipeline_disable_joins
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68870
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340283
-milestone: '14.3'
-type: development
-group: group::release
-default_enabled: true
diff --git a/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml b/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml
new file mode 100644
index 0000000000..8987b729ca
--- /dev/null
+++ b/config/feature_flags/development/finding_ci_pipeline_disable_joins.yml
@@ -0,0 +1,8 @@
+---
+name: finding_ci_pipeline_disable_joins
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70216
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338665
+milestone: '14.3'
+type: development
+group: group::threat insights
+default_enabled: true
diff --git a/config/feature_flags/development/gitaly_tags_finder.yml b/config/feature_flags/development/gitaly_tags_finder.yml
deleted file mode 100644
index a0a1791e58..0000000000
--- a/config/feature_flags/development/gitaly_tags_finder.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: gitaly_tags_finder
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69101
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
-milestone: '14.3'
-type: development
-group: group::source code
-default_enabled: false
diff --git a/config/feature_flags/development/gitaly_user_merge_branch_access_error.yml b/config/feature_flags/development/gitaly_user_merge_branch_access_error.yml
new file mode 100644
index 0000000000..6e52112b75
--- /dev/null
+++ b/config/feature_flags/development/gitaly_user_merge_branch_access_error.yml
@@ -0,0 +1,8 @@
+---
+name: gitaly_user_merge_branch_access_error
+introduced_by_url: https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3705
+rollout_issue_url: https://gitlab.com/gitlab-org/gitaly/-/issues/3757
+milestone: '14.3'
+type: development
+group: group::gitaly
+default_enabled: false
diff --git a/config/feature_flags/development/help_page_documentation_redirect.yml b/config/feature_flags/development/help_page_documentation_redirect.yml
deleted file mode 100644
index 8871160e42..0000000000
--- a/config/feature_flags/development/help_page_documentation_redirect.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: help_page_documentation_redirect
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/42702
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/255328
-milestone: '13.5'
-type: development
-group: group::static site editor
-default_enabled: false
diff --git a/config/feature_flags/development/improved_mergeability_checks.yml b/config/feature_flags/development/improved_mergeability_checks.yml
new file mode 100644
index 0000000000..83450ffa16
--- /dev/null
+++ b/config/feature_flags/development/improved_mergeability_checks.yml
@@ -0,0 +1,8 @@
+---
+name: improved_mergeability_checks
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68312
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342386
+milestone: '14.4'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/incubation_5mp_google_cloud.yml b/config/feature_flags/development/incubation_5mp_google_cloud.yml
new file mode 100644
index 0000000000..b687a656b4
--- /dev/null
+++ b/config/feature_flags/development/incubation_5mp_google_cloud.yml
@@ -0,0 +1,8 @@
+---
+name: incubation_5mp_google_cloud
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70715
+rollout_issue_url:
+milestone: '14.3'
+type: development
+group: group::incubation
+default_enabled: false
diff --git a/config/feature_flags/development/infinitely_collapsible_sections.yml b/config/feature_flags/development/infinitely_collapsible_sections.yml
index d0bf063c6f..44f37c06d7 100644
--- a/config/feature_flags/development/infinitely_collapsible_sections.yml
+++ b/config/feature_flags/development/infinitely_collapsible_sections.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335297
milestone: '14.1'
type: development
group: group::pipeline execution
-default_enabled: true
+default_enabled: false
diff --git a/config/feature_flags/development/jira_connect_asymmetric_jwt.yml b/config/feature_flags/development/jira_connect_asymmetric_jwt.yml
new file mode 100644
index 0000000000..e204a7d6fa
--- /dev/null
+++ b/config/feature_flags/development/jira_connect_asymmetric_jwt.yml
@@ -0,0 +1,8 @@
+---
+name: jira_connect_asymmetric_jwt
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71080
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342808
+milestone: '14.4'
+type: development
+group: group::integrations
+default_enabled: false
diff --git a/config/feature_flags/development/lazy_load_commits.yml b/config/feature_flags/development/lazy_load_commits.yml
new file mode 100644
index 0000000000..d476490721
--- /dev/null
+++ b/config/feature_flags/development/lazy_load_commits.yml
@@ -0,0 +1,8 @@
+---
+name: lazy_load_commits
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71633
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342497
+milestone: '14.4'
+type: development
+group: group::source code
+default_enabled: false
diff --git a/config/feature_flags/development/linear_application_setting_ancestor_scopes.yml b/config/feature_flags/development/linear_application_setting_ancestor_scopes.yml
new file mode 100644
index 0000000000..18c64df78d
--- /dev/null
+++ b/config/feature_flags/development/linear_application_setting_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_application_setting_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70579
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341346
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_ee_group_ancestor_scopes.yml b/config/feature_flags/development/linear_ee_group_ancestor_scopes.yml
new file mode 100644
index 0000000000..46294b0aef
--- /dev/null
+++ b/config/feature_flags/development/linear_ee_group_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_ee_group_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70708
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341350
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_group_ancestor_scopes.yml b/config/feature_flags/development/linear_group_ancestor_scopes.yml
new file mode 100644
index 0000000000..f23399c1e6
--- /dev/null
+++ b/config/feature_flags/development/linear_group_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_group_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70495
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341115
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml b/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml
new file mode 100644
index 0000000000..d45b8d71a2
--- /dev/null
+++ b/config/feature_flags/development/linear_group_plans_preloaded_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_group_plans_preloaded_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70685
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341349
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_group_tree_ancestor_scopes.yml b/config/feature_flags/development/linear_group_tree_ancestor_scopes.yml
new file mode 100644
index 0000000000..3a195242fa
--- /dev/null
+++ b/config/feature_flags/development/linear_group_tree_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_group_tree_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70503
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341117
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml b/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml
new file mode 100644
index 0000000000..6bd5e16432
--- /dev/null
+++ b/config/feature_flags/development/linear_members_finder_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_members_finder_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70583
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341347
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/linear_participants_service_ancestor_scopes.yml b/config/feature_flags/development/linear_participants_service_ancestor_scopes.yml
new file mode 100644
index 0000000000..41b6f3b32d
--- /dev/null
+++ b/config/feature_flags/development/linear_participants_service_ancestor_scopes.yml
@@ -0,0 +1,8 @@
+---
+name: linear_participants_service_ancestor_scopes
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70684
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341348
+milestone: '14.4'
+type: development
+group: group::access
+default_enabled: false
diff --git a/config/feature_flags/development/merge_request_discussion_cache.yml b/config/feature_flags/development/merge_request_discussion_cache.yml
deleted file mode 100644
index e90887fc2b..0000000000
--- a/config/feature_flags/development/merge_request_discussion_cache.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: merge_request_discussion_cache
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64688
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/335799
-milestone: '14.1'
-type: development
-group: group::code review
-default_enabled: false
diff --git a/config/feature_flags/development/mergeability_caching.yml b/config/feature_flags/development/mergeability_caching.yml
new file mode 100644
index 0000000000..b906329992
--- /dev/null
+++ b/config/feature_flags/development/mergeability_caching.yml
@@ -0,0 +1,8 @@
+---
+name: mergeability_caching
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68312
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340810
+milestone: '14.4'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/mr_changes_fluid_layout.yml b/config/feature_flags/development/mr_changes_fluid_layout.yml
new file mode 100644
index 0000000000..87f0c0c656
--- /dev/null
+++ b/config/feature_flags/development/mr_changes_fluid_layout.yml
@@ -0,0 +1,8 @@
+---
+name: mr_changes_fluid_layout
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70815
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341809
+milestone: '14.4'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/new_customersdot_staging_url.yml b/config/feature_flags/development/new_customersdot_staging_url.yml
new file mode 100644
index 0000000000..288d7f66f0
--- /dev/null
+++ b/config/feature_flags/development/new_customersdot_staging_url.yml
@@ -0,0 +1,8 @@
+---
+name: new_customersdot_staging_url
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71827
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342513
+milestone: '14.4'
+type: development
+group: group::fulfillment
+default_enabled: false
diff --git a/config/feature_flags/development/new_dir_modal.yml b/config/feature_flags/development/new_dir_modal.yml
new file mode 100644
index 0000000000..12d007209b
--- /dev/null
+++ b/config/feature_flags/development/new_dir_modal.yml
@@ -0,0 +1,8 @@
+---
+name: new_dir_modal
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71154
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341675
+milestone: '14.4'
+type: development
+group: group::source code
+default_enabled: true
diff --git a/config/feature_flags/development/operational_vulnerabilities.yml b/config/feature_flags/development/operational_vulnerabilities.yml
new file mode 100644
index 0000000000..f1e19a626f
--- /dev/null
+++ b/config/feature_flags/development/operational_vulnerabilities.yml
@@ -0,0 +1,8 @@
+---
+name: operational_vulnerabilities
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70732
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341423
+milestone: '14.4'
+type: development
+group: group::container security
+default_enabled: false
diff --git a/config/feature_flags/development/package_list_apollo.yml b/config/feature_flags/development/package_list_apollo.yml
new file mode 100644
index 0000000000..522b08594e
--- /dev/null
+++ b/config/feature_flags/development/package_list_apollo.yml
@@ -0,0 +1,8 @@
+---
+name: package_list_apollo
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70598
+rollout_issue_url:
+milestone: '14.3'
+type: development
+group: group::package
+default_enabled: false
diff --git a/config/feature_flags/development/pages_smart_check_outdated_sha.yml b/config/feature_flags/development/pages_smart_check_outdated_sha.yml
deleted file mode 100644
index 528d357f65..0000000000
--- a/config/feature_flags/development/pages_smart_check_outdated_sha.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: pages_smart_check_outdated_sha
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67303
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/336574
-milestone: '14.2'
-type: development
-group: group::release
-default_enabled: false
diff --git a/config/feature_flags/development/paginated_tree_graphql_query.yml b/config/feature_flags/development/paginated_tree_graphql_query.yml
index 13096412f2..d56d8fc336 100644
--- a/config/feature_flags/development/paginated_tree_graphql_query.yml
+++ b/config/feature_flags/development/paginated_tree_graphql_query.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337214
milestone: '14.2'
type: development
group: group::source code
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/pipeline_editor_drawer.yml b/config/feature_flags/development/pipeline_editor_drawer.yml
deleted file mode 100644
index df73c4be01..0000000000
--- a/config/feature_flags/development/pipeline_editor_drawer.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: pipeline_editor_drawer
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60856
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/329806
-milestone: '13.12'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/pipeline_editor_empty_state_action.yml b/config/feature_flags/development/pipeline_editor_empty_state_action.yml
deleted file mode 100644
index 870aeb1493..0000000000
--- a/config/feature_flags/development/pipeline_editor_empty_state_action.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: pipeline_editor_empty_state_action
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/55414
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/323229
-milestone: '13.10'
-type: development
-group: group::pipeline authoring
-default_enabled: true
diff --git a/config/feature_flags/development/pipeline_editor_mini_graph.yml b/config/feature_flags/development/pipeline_editor_mini_graph.yml
new file mode 100644
index 0000000000..6f31cb18d8
--- /dev/null
+++ b/config/feature_flags/development/pipeline_editor_mini_graph.yml
@@ -0,0 +1,8 @@
+---
+name: pipeline_editor_mini_graph
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71622
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342217
+milestone: '14.4'
+type: development
+group: group::pipeline authoring
+default_enabled: false
diff --git a/config/feature_flags/development/rate_limited_service_issues_create.yml b/config/feature_flags/development/rate_limited_service_issues_create.yml
new file mode 100644
index 0000000000..95ece10aa6
--- /dev/null
+++ b/config/feature_flags/development/rate_limited_service_issues_create.yml
@@ -0,0 +1,8 @@
+---
+name: rate_limited_service_issues_create
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68526
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342677
+milestone: '14.4'
+type: development
+group: group::project management
+default_enabled: false
diff --git a/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml b/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml
deleted file mode 100644
index 37c475067a..0000000000
--- a/config/feature_flags/development/redirect_to_latest_template_jobs_browser_performance_testing.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: redirect_to_latest_template_jobs_browser_performance_testing
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
-rollout_issue_url:
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml b/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml
deleted file mode 100644
index 96606515bd..0000000000
--- a/config/feature_flags/development/redirect_to_latest_template_security_api_fuzzing.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: redirect_to_latest_template_security_api_fuzzing
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
-rollout_issue_url:
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/redirect_to_latest_template_security_dast.yml b/config/feature_flags/development/redirect_to_latest_template_security_dast.yml
deleted file mode 100644
index a95c1e1a04..0000000000
--- a/config/feature_flags/development/redirect_to_latest_template_security_dast.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: redirect_to_latest_template_security_dast
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
-rollout_issue_url:
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/redirect_to_latest_template_terraform.yml b/config/feature_flags/development/redirect_to_latest_template_terraform.yml
deleted file mode 100644
index cb5d833fa2..0000000000
--- a/config/feature_flags/development/redirect_to_latest_template_terraform.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: redirect_to_latest_template_terraform
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
-rollout_issue_url:
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml b/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml
deleted file mode 100644
index 4df74a5b07..0000000000
--- a/config/feature_flags/development/redirect_to_latest_template_verify_browser_performance.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: redirect_to_latest_template_verify_browser_performance
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63144
-rollout_issue_url:
-milestone: '14.0'
-type: development
-group: group::pipeline authoring
-default_enabled: false
diff --git a/config/feature_flags/development/refactor_mr_widgets_extensions.yml b/config/feature_flags/development/refactor_mr_widgets_extensions.yml
new file mode 100644
index 0000000000..5b6ea22aaf
--- /dev/null
+++ b/config/feature_flags/development/refactor_mr_widgets_extensions.yml
@@ -0,0 +1,8 @@
+---
+name: refactor_mr_widgets_extensions
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70993
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341759
+milestone: '14.4'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/refactor_mr_widgets_extensions_user.yml b/config/feature_flags/development/refactor_mr_widgets_extensions_user.yml
new file mode 100644
index 0000000000..aa3c279910
--- /dev/null
+++ b/config/feature_flags/development/refactor_mr_widgets_extensions_user.yml
@@ -0,0 +1,8 @@
+---
+name: refactor_mr_widgets_extensions_user
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70993
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341759
+milestone: '14.4'
+type: development
+group: group::code review
+default_enabled: false
diff --git a/config/feature_flags/development/refactor_text_viewer.yml b/config/feature_flags/development/refactor_text_viewer.yml
new file mode 100644
index 0000000000..427137773c
--- /dev/null
+++ b/config/feature_flags/development/refactor_text_viewer.yml
@@ -0,0 +1,8 @@
+---
+name: refactor_text_viewer
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70909
+rollout_issue_url:
+milestone: '14.4'
+type: development
+group: 'group::source code'
+default_enabled: false
diff --git a/config/feature_flags/development/reference_cache_memoization.yml b/config/feature_flags/development/reference_cache_memoization.yml
new file mode 100644
index 0000000000..7401220817
--- /dev/null
+++ b/config/feature_flags/development/reference_cache_memoization.yml
@@ -0,0 +1,8 @@
+---
+name: reference_cache_memoization
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71310
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/341849
+milestone: '14.4'
+type: development
+group: group::source code
+default_enabled: true
diff --git a/config/feature_flags/development/remove_composer_v1_cache_code.yml b/config/feature_flags/development/remove_composer_v1_cache_code.yml
deleted file mode 100644
index 9654fc8dc5..0000000000
--- a/config/feature_flags/development/remove_composer_v1_cache_code.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: remove_composer_v1_cache_code
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67843
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338264
-milestone: '14.2'
-type: development
-group: group::package
-default_enabled: false
diff --git a/config/feature_flags/development/request_apdex_counters.yml b/config/feature_flags/development/request_apdex_counters.yml
new file mode 100644
index 0000000000..07d6cb7ac5
--- /dev/null
+++ b/config/feature_flags/development/request_apdex_counters.yml
@@ -0,0 +1,8 @@
+---
+name: request_apdex_counters
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69154
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1099
+milestone: '14.3'
+type: development
+group: team::Scalability
+default_enabled: false
diff --git a/config/feature_flags/development/search_blobs_language_aggregation.yml b/config/feature_flags/development/search_blobs_language_aggregation.yml
new file mode 100644
index 0000000000..da1b81dc52
--- /dev/null
+++ b/config/feature_flags/development/search_blobs_language_aggregation.yml
@@ -0,0 +1,8 @@
+---
+name: search_blobs_language_aggregation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71937
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342621
+milestone: '14.4'
+type: development
+group: group::global search
+default_enabled: false
diff --git a/config/feature_flags/development/security_orchestration_policies_configuration.yml b/config/feature_flags/development/security_orchestration_policies_configuration.yml
deleted file mode 100644
index 2570743c10..0000000000
--- a/config/feature_flags/development/security_orchestration_policies_configuration.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: security_orchestration_policies_configuration
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/54220
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/321258
-milestone: '13.9'
-type: development
-group: group::container security
-default_enabled: true
diff --git a/config/feature_flags/development/security_report_ingestion_framework.yml b/config/feature_flags/development/security_report_ingestion_framework.yml
new file mode 100644
index 0000000000..490fd03c67
--- /dev/null
+++ b/config/feature_flags/development/security_report_ingestion_framework.yml
@@ -0,0 +1,8 @@
+---
+name: security_report_ingestion_framework
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66735
+rollout_issue_url:
+milestone: '14.4'
+type: development
+group: group::threat insights
+default_enabled: false
diff --git a/config/feature_flags/development/serverless_domain.yml b/config/feature_flags/development/serverless_domain.yml
deleted file mode 100644
index 67b2c6b8e1..0000000000
--- a/config/feature_flags/development/serverless_domain.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: serverless_domain
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/21222
-rollout_issue_url:
-milestone: '12.8'
-type: development
-group: group::configure
-default_enabled: false
diff --git a/config/feature_flags/development/show_author_on_note.yml b/config/feature_flags/development/show_author_on_note.yml
deleted file mode 100644
index 7775bf5f27..0000000000
--- a/config/feature_flags/development/show_author_on_note.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: show_author_on_note
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/40198
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/250282
-milestone: '13.4'
-type: development
-group: group::project management
-default_enabled: false
diff --git a/config/feature_flags/development/sort_by_project_users_by_project_authorizations_user_id.yml b/config/feature_flags/development/sort_by_project_users_by_project_authorizations_user_id.yml
deleted file mode 100644
index 88a4e0b047..0000000000
--- a/config/feature_flags/development/sort_by_project_users_by_project_authorizations_user_id.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: sort_by_project_users_by_project_authorizations_user_id
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/64528
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334167
-milestone: '14.1'
-type: development
-group: group::optimize
-default_enabled: true
diff --git a/config/feature_flags/development/specialized_worker_for_project_share_update_auth_recalculation.yml b/config/feature_flags/development/specialized_worker_for_project_share_update_auth_recalculation.yml
deleted file mode 100644
index 5e7d3819a4..0000000000
--- a/config/feature_flags/development/specialized_worker_for_project_share_update_auth_recalculation.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: specialized_worker_for_project_share_update_auth_recalculation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61964
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334234
-milestone: '14.1'
-type: development
-group: group::access
-default_enabled: false
diff --git a/config/feature_flags/development/specialized_worker_for_project_transfer_auth_recalculation.yml b/config/feature_flags/development/specialized_worker_for_project_transfer_auth_recalculation.yml
deleted file mode 100644
index b77ed60750..0000000000
--- a/config/feature_flags/development/specialized_worker_for_project_transfer_auth_recalculation.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: specialized_worker_for_project_transfer_auth_recalculation
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/61967
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/334237
-milestone: '14.1'
-type: development
-group: group::access
-default_enabled: false
diff --git a/config/feature_flags/development/suppress_apollo_errors_during_navigation.yml b/config/feature_flags/development/suppress_apollo_errors_during_navigation.yml
new file mode 100644
index 0000000000..21548fa4db
--- /dev/null
+++ b/config/feature_flags/development/suppress_apollo_errors_during_navigation.yml
@@ -0,0 +1,8 @@
+---
+name: suppress_apollo_errors_during_navigation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/72031
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342745
+milestone: '14.4'
+type: development
+group: group::foundations
+default_enabled: false
diff --git a/config/feature_flags/development/surface_environment_creation_failure.yml b/config/feature_flags/development/surface_environment_creation_failure.yml
new file mode 100644
index 0000000000..2c312d432e
--- /dev/null
+++ b/config/feature_flags/development/surface_environment_creation_failure.yml
@@ -0,0 +1,8 @@
+---
+name: surface_environment_creation_failure
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169
+milestone: '14.4'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/surface_environment_creation_failure_override.yml b/config/feature_flags/development/surface_environment_creation_failure_override.yml
new file mode 100644
index 0000000000..566281bcb8
--- /dev/null
+++ b/config/feature_flags/development/surface_environment_creation_failure_override.yml
@@ -0,0 +1,8 @@
+---
+name: surface_environment_creation_failure_override
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69537
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340169
+milestone: '14.4'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/tags_finder_gitaly.yml b/config/feature_flags/development/tags_finder_gitaly.yml
new file mode 100644
index 0000000000..065a253a69
--- /dev/null
+++ b/config/feature_flags/development/tags_finder_gitaly.yml
@@ -0,0 +1,8 @@
+---
+name: tags_finder_gitaly
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69101
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339741
+milestone: '14.3'
+type: development
+group: group::source code
+default_enabled: true
diff --git a/config/feature_flags/development/track_epic_boards_activity.yml b/config/feature_flags/development/track_epic_boards_activity.yml
deleted file mode 100644
index df48cc5a85..0000000000
--- a/config/feature_flags/development/track_epic_boards_activity.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: track_epic_boards_activity
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60357
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/338038
-milestone: '13.12'
-type: development
-group: group::product planning
-default_enabled: true
diff --git a/config/feature_flags/development/track_importer_activity.yml b/config/feature_flags/development/track_importer_activity.yml
new file mode 100644
index 0000000000..9f20a14790
--- /dev/null
+++ b/config/feature_flags/development/track_importer_activity.yml
@@ -0,0 +1,8 @@
+---
+name: track_importer_activity
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339392
+milestone: '14.4'
+type: development
+group: group::import
+default_enabled: false
diff --git a/config/feature_flags/development/update_deployment_after_transaction_commit.yml b/config/feature_flags/development/update_deployment_after_transaction_commit.yml
new file mode 100644
index 0000000000..c07622fc9b
--- /dev/null
+++ b/config/feature_flags/development/update_deployment_after_transaction_commit.yml
@@ -0,0 +1,8 @@
+---
+name: update_deployment_after_transaction_commit
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71450
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/342021
+milestone: '14.4'
+type: development
+group: group::release
+default_enabled: false
diff --git a/config/feature_flags/development/usage_data_i_testing_group_code_coverage_visit_total.yml b/config/feature_flags/development/usage_data_i_testing_group_code_coverage_visit_total.yml
deleted file mode 100644
index 720b94fcf6..0000000000
--- a/config/feature_flags/development/usage_data_i_testing_group_code_coverage_visit_total.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: usage_data_i_testing_group_code_coverage_visit_total
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51382
-rollout_issue_url:
-milestone: '13.8'
-type: development
-group: group::testing
-default_enabled: true
diff --git a/config/feature_flags/development/usage_data_i_testing_metrics_report_artifact_uploaders.yml b/config/feature_flags/development/usage_data_i_testing_metrics_report_artifact_uploaders.yml
deleted file mode 100644
index 968ab3e63f..0000000000
--- a/config/feature_flags/development/usage_data_i_testing_metrics_report_artifact_uploaders.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: usage_data_i_testing_metrics_report_artifact_uploaders
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/51670
-rollout_issue_url:
-milestone: '13.9'
-type: development
-group: group::testing
-default_enabled: true
diff --git a/config/feature_flags/development/usage_data_i_testing_summary_widget_total.yml b/config/feature_flags/development/usage_data_i_testing_summary_widget_total.yml
deleted file mode 100644
index fb06ea9f58..0000000000
--- a/config/feature_flags/development/usage_data_i_testing_summary_widget_total.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: usage_data_i_testing_summary_widget_total
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/57543
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/326058
-milestone: '13.11'
-type: development
-group: group::testing
-default_enabled: true
diff --git a/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml b/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml
deleted file mode 100644
index e6e3cd09c2..0000000000
--- a/config/feature_flags/development/usage_data_i_testing_test_case_parsed.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: usage_data_i_testing_test_case_parsed
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/41918
-rollout_issue_url:
-milestone: '13.5'
-type: development
-group: group::testing
-default_enabled: true
diff --git a/config/feature_flags/development/use_upsert_query_for_mr_metrics.yml b/config/feature_flags/development/use_upsert_query_for_mr_metrics.yml
index 14cc5d1a98..605bc54b78 100644
--- a/config/feature_flags/development/use_upsert_query_for_mr_metrics.yml
+++ b/config/feature_flags/development/use_upsert_query_for_mr_metrics.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/339677
milestone: '14.3'
type: development
group: group::optimize
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/validate_namespace_parent_type.yml b/config/feature_flags/development/validate_namespace_parent_type.yml
index dc89c462f1..5c2e0add24 100644
--- a/config/feature_flags/development/validate_namespace_parent_type.yml
+++ b/config/feature_flags/development/validate_namespace_parent_type.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/322101
milestone: '13.10'
type: development
group: group::access
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/variable_inside_variable.yml b/config/feature_flags/development/variable_inside_variable.yml
index 2060958590..fee4897b3f 100644
--- a/config/feature_flags/development/variable_inside_variable.yml
+++ b/config/feature_flags/development/variable_inside_variable.yml
@@ -5,4 +5,4 @@ rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/297382
milestone: '13.11'
type: development
group: group::runner
-default_enabled: false
+default_enabled: true
diff --git a/config/feature_flags/development/vulnerability_flags.yml b/config/feature_flags/development/vulnerability_flags.yml
deleted file mode 100644
index 6ea7dd2e3f..0000000000
--- a/config/feature_flags/development/vulnerability_flags.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: vulnerability_flags
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/66775
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340203
-milestone: '14.3'
-type: development
-group: group::static analysis
-default_enabled: true
diff --git a/config/feature_flags/development/vulnerability_location_image_filter.yml b/config/feature_flags/development/vulnerability_location_image_filter.yml
new file mode 100644
index 0000000000..4b373b76ff
--- /dev/null
+++ b/config/feature_flags/development/vulnerability_location_image_filter.yml
@@ -0,0 +1,8 @@
+---
+name: vulnerability_location_image_filter
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69867
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340915
+milestone: '14.4'
+type: development
+group: group::container security
+default_enabled: false
diff --git a/config/feature_flags/development/workhorse_use_sidechannel.yml b/config/feature_flags/development/workhorse_use_sidechannel.yml
new file mode 100644
index 0000000000..f39d313bf1
--- /dev/null
+++ b/config/feature_flags/development/workhorse_use_sidechannel.yml
@@ -0,0 +1,8 @@
+---
+name: workhorse_use_sidechannel
+introduced_by_url:
+rollout_issue_url: https://gitlab.com/gitlab-com/gl-infra/scalability/-/issues/1193
+milestone: '14.4'
+type: development
+group: 'group::scalability'
+default_enabled: false
diff --git a/config/feature_flags/experiment/jobs_to_be_done.yml b/config/feature_flags/experiment/jobs_to_be_done.yml
deleted file mode 100644
index 5589d33a3c..0000000000
--- a/config/feature_flags/experiment/jobs_to_be_done.yml
+++ /dev/null
@@ -1,8 +0,0 @@
----
-name: jobs_to_be_done
-introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/60038
-rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/285564
-milestone: '13.12'
-type: experiment
-group: group::adoption
-default_enabled: false
diff --git a/config/feature_flags/experiment/new_project_sast_enabled.yml b/config/feature_flags/experiment/new_project_sast_enabled.yml
new file mode 100644
index 0000000000..f47c01d26a
--- /dev/null
+++ b/config/feature_flags/experiment/new_project_sast_enabled.yml
@@ -0,0 +1,8 @@
+---
+name: new_project_sast_enabled
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70548
+rollout_issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/340929
+milestone: '14.4'
+type: experiment
+group: group::adoption
+default_enabled: false
diff --git a/config/feature_flags/ops/ecomm_instrumentation.yml b/config/feature_flags/ops/ecomm_instrumentation.yml
new file mode 100644
index 0000000000..e35937fa34
--- /dev/null
+++ b/config/feature_flags/ops/ecomm_instrumentation.yml
@@ -0,0 +1,8 @@
+---
+name: ecomm_instrumentation
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71243
+rollout_issue_url:
+milestone: '14.4'
+type: ops
+group: group::product intelligence
+default_enabled: false
diff --git a/config/feature_flags/ops/show_terraform_banner.yml b/config/feature_flags/ops/show_terraform_banner.yml
new file mode 100644
index 0000000000..a4ec831f4e
--- /dev/null
+++ b/config/feature_flags/ops/show_terraform_banner.yml
@@ -0,0 +1,8 @@
+---
+name: show_terraform_banner
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71462
+rollout_issue_url:
+milestone: '14.4'
+type: ops
+group: group::configure
+default_enabled: true
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 55b165b192..bb69c215f8 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -1220,6 +1220,9 @@ production: &base
# The URL to the internal KAS API (used by the GitLab backend)
# internal_url: grpc://localhost:8153
+ # The URL to the Kubernetes API proxy (used by GitLab users)
+ # external_k8s_proxy_url: https://localhost:8154 # default: nil
+
## GitLab Elasticsearch settings
elasticsearch:
indexer_path: /home/git/gitlab-elasticsearch-indexer/
@@ -1294,6 +1297,9 @@ production: &base
## Google tag manager
# google_tag_manager_id: '_your_tracking_id'
+ ## OneTrust
+ # one_trust_id: '_your_one_trust_id'
+
## Matomo analytics.
# matomo_url: '_your_matomo_url'
# matomo_site_id: '_your_matomo_site_id'
diff --git a/config/initializers/00_active_record_disable_joins.rb b/config/initializers/00_active_record_disable_joins.rb
new file mode 100644
index 0000000000..6348fd7c53
--- /dev/null
+++ b/config/initializers/00_active_record_disable_joins.rb
@@ -0,0 +1,11 @@
+# frozen_string_literal: true
+
+module ActiveRecordRelationAllowCrossJoins
+ def allow_cross_joins_across_databases(url:)
+ # this method is implemented in:
+ # spec/support/database/prevent_cross_joins.rb
+ self
+ end
+end
+
+ActiveRecord::Relation.prepend(ActiveRecordRelationAllowCrossJoins)
diff --git a/config/initializers/0_acts_as_taggable.rb b/config/initializers/0_acts_as_taggable.rb
index 04619590e3..8dee3c52a5 100644
--- a/config/initializers/0_acts_as_taggable.rb
+++ b/config/initializers/0_acts_as_taggable.rb
@@ -11,8 +11,8 @@ raise "Counter cache is not disabled" if
ActsAsTaggableOn::Tagging.reflections["tag"].options[:counter_cache]
ActsAsTaggableOn::Tagging.include IgnorableColumns
-ActsAsTaggableOn::Tagging.ignore_column :id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
-ActsAsTaggableOn::Tagging.ignore_column :taggable_id_convert_to_bigint, remove_with: '14.2', remove_after: '2021-08-22'
+ActsAsTaggableOn::Tagging.ignore_column :id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
+ActsAsTaggableOn::Tagging.ignore_column :taggable_id_convert_to_bigint, remove_with: '14.5', remove_after: '2021-10-22'
# The tags and taggings are supposed to be part of `gitlab_ci`
ActsAsTaggableOn::Tag.gitlab_schema = :gitlab_ci
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index e13061655c..d6957491b1 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -465,9 +465,6 @@ Settings.cron_jobs['personal_access_tokens_expired_notification_worker']['job_cl
Settings.cron_jobs['repository_archive_cache_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['repository_archive_cache_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['repository_archive_cache_worker']['job_class'] = 'RepositoryArchiveCacheWorker'
-Settings.cron_jobs['packages_composer_cache_cleanup_worker'] ||= Settingslogic.new({})
-Settings.cron_jobs['packages_composer_cache_cleanup_worker']['cron'] ||= '30 * * * *'
-Settings.cron_jobs['packages_composer_cache_cleanup_worker']['job_class'] = 'Packages::Composer::CacheCleanupWorker'
Settings.cron_jobs['import_export_project_cleanup_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['import_export_project_cleanup_worker']['cron'] ||= '0 * * * *'
Settings.cron_jobs['import_export_project_cleanup_worker']['job_class'] = 'ImportExportProjectCleanupWorker'
@@ -534,6 +531,9 @@ Settings.cron_jobs['namespaces_prune_aggregation_schedules_worker']['job_class']
Settings.cron_jobs['container_expiration_policy_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['container_expiration_policy_worker']['cron'] ||= '50 * * * *'
Settings.cron_jobs['container_expiration_policy_worker']['job_class'] = 'ContainerExpirationPolicyWorker'
+Settings.cron_jobs['image_ttl_group_policy_worker'] ||= Settingslogic.new({})
+Settings.cron_jobs['image_ttl_group_policy_worker']['cron'] ||= '40 0 * * *'
+Settings.cron_jobs['image_ttl_group_policy_worker']['job_class'] = 'DependencyProxy::ImageTtlGroupPolicyWorker'
Settings.cron_jobs['x509_issuer_crl_check_worker'] ||= Settingslogic.new({})
Settings.cron_jobs['x509_issuer_crl_check_worker']['cron'] ||= '30 1 * * *'
Settings.cron_jobs['x509_issuer_crl_check_worker']['job_class'] = 'X509IssuerCrlCheckWorker'
@@ -756,6 +756,7 @@ Settings.gitlab_kas['enabled'] ||= false
Settings.gitlab_kas['secret_file'] ||= Rails.root.join('.gitlab_kas_secret')
Settings.gitlab_kas['external_url'] ||= 'wss://kas.example.com'
Settings.gitlab_kas['internal_url'] ||= 'grpc://localhost:8153'
+# Settings.gitlab_kas['external_k8s_proxy_url'] ||= 'grpc://localhost:8154' # NOTE: Do not set a default until all distributions have been updated with a correct value
#
# Repositories
diff --git a/config/initializers/7_prometheus_metrics.rb b/config/initializers/7_prometheus_metrics.rb
index d2d546a543..587d393fd7 100644
--- a/config/initializers/7_prometheus_metrics.rb
+++ b/config/initializers/7_prometheus_metrics.rb
@@ -48,7 +48,7 @@ if !Rails.env.test? && Gitlab::Metrics.prometheus_metrics_enabled?
Gitlab::Metrics.gauge(:deployments, 'GitLab Version', {}, :max).set({ version: Gitlab::VERSION, revision: Gitlab.revision }, 1)
- unless Gitlab::Runtime.sidekiq?
+ if Gitlab::Runtime.web_server?
Gitlab::Metrics::RequestsRackMiddleware.initialize_metrics
end
diff --git a/config/initializers/7_redis.rb b/config/initializers/7_redis.rb
index 84aa231089..50f0fb9231 100644
--- a/config/initializers/7_redis.rb
+++ b/config/initializers/7_redis.rb
@@ -1,5 +1,11 @@
# frozen_string_literal: true
+# We set the instance variable directly to suppress warnings.
+# We cannot switch to the new behavior until we change all existing `redis.exists` calls to `redis.exists?`.
+# Some gems also need to be updated
+# https://gitlab.com/gitlab-org/gitlab/-/issues/340602
+Redis.instance_variable_set(:@exists_returns_integer, false)
+
Redis::Client.prepend(Gitlab::Instrumentation::RedisInterceptor)
# Make sure we initialize a Redis connection pool before multi-threaded
@@ -11,3 +17,5 @@ Gitlab::Redis::Cache.with { nil }
Gitlab::Redis::Queues.with { nil }
Gitlab::Redis::SharedState.with { nil }
Gitlab::Redis::TraceChunks.with { nil }
+Gitlab::Redis::RateLimiting.with { nil }
+Gitlab::Redis::Sessions.with { nil }
diff --git a/config/initializers/action_cable.rb b/config/initializers/action_cable.rb
index 16d29f5910..a7ef5cc332 100644
--- a/config/initializers/action_cable.rb
+++ b/config/initializers/action_cable.rb
@@ -19,4 +19,4 @@ ActionCable::SubscriptionAdapter::Redis.redis_connector = lambda do |config|
end
Gitlab::ActionCable::RequestStoreCallbacks.install
-Gitlab::Database::LoadBalancing::ActionCableCallbacks.install if Gitlab::Database::LoadBalancing.enable?
+Gitlab::Database::LoadBalancing::ActionCableCallbacks.install
diff --git a/config/initializers/backtrace_silencers.rb b/config/initializers/backtrace_silencers.rb
index f3e01d2344..4fd41bd4c7 100644
--- a/config/initializers/backtrace_silencers.rb
+++ b/config/initializers/backtrace_silencers.rb
@@ -4,7 +4,7 @@ Rails.backtrace_cleaner.remove_silencers!
# This allows us to see the proper caller of SQL calls in {development,test}.log
if (Rails.env.development? || Rails.env.test?) && Gitlab.ee?
- Rails.backtrace_cleaner.add_silencer { |line| %r(^ee/lib/gitlab/database/load_balancing).match?(line) }
+ Rails.backtrace_cleaner.add_silencer { |line| %r(^lib/gitlab/database/load_balancing).match?(line) }
end
Rails.backtrace_cleaner.add_silencer { |line| !Gitlab::APP_DIRS_PATTERN.match?(line) }
diff --git a/config/initializers/batch_loader.rb b/config/initializers/batch_loader.rb
index d88b43fbce..e542a26cb8 100644
--- a/config/initializers/batch_loader.rb
+++ b/config/initializers/batch_loader.rb
@@ -1,3 +1,13 @@
# frozen_string_literal: true
Rails.application.config.middleware.use(BatchLoader::Middleware)
+
+# Disables replace_methods by default.
+# See https://github.com/exAspArk/batch-loader#replacing-methods for more information.
+module BatchLoaderWithoutMethodReplacementByDefault
+ def batch(replace_methods: false, **kw_args, &batch_block)
+ super
+ end
+end
+
+BatchLoader.prepend(BatchLoaderWithoutMethodReplacementByDefault)
diff --git a/config/initializers/carrierwave_patch.rb b/config/initializers/carrierwave_patch.rb
index c8c6f75949..a9c7447854 100644
--- a/config/initializers/carrierwave_patch.rb
+++ b/config/initializers/carrierwave_patch.rb
@@ -49,14 +49,8 @@ module CarrierWave
local_file = local_directory.files.new(key: path)
expire_at = options[:expire_at] || ::Fog::Time.now + @uploader.fog_authenticated_url_expiration
case @uploader.fog_credentials[:provider]
- when 'AWS', 'Google'
- # Older versions of fog-google do not support options as a parameter
- if url_options_supported?(local_file)
- local_file.url(expire_at, options)
- else
- warn "Options hash not supported in #{local_file.class}. You may need to upgrade your Fog provider."
- local_file.url(expire_at)
- end
+ when 'AWS', 'Google', 'AzureRM'
+ local_file.url(expire_at, options)
when 'Rackspace'
connection.get_object_https_url(@uploader.fog_directory, path, expire_at, options)
when 'OpenStack'
diff --git a/config/initializers/database_config.rb b/config/initializers/database_config.rb
index e9f10abd0b..7aedf9013a 100644
--- a/config/initializers/database_config.rb
+++ b/config/initializers/database_config.rb
@@ -1,15 +1,5 @@
# frozen_string_literal: true
-def log_pool_size(db, previous_pool_size, current_pool_size)
- log_message = ["#{db} connection pool size: #{current_pool_size}"]
-
- if previous_pool_size && current_pool_size > previous_pool_size
- log_message << "(increased from #{previous_pool_size} to match thread count)"
- end
-
- Gitlab::AppLogger.debug(log_message.join(' '))
-end
-
Gitlab.ee do
# We need to initialize the Geo database before
# setting the Geo DB connection pool size.
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index c23049e93c..3c994516b2 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -1,30 +1,8 @@
# frozen_string_literal: true
-require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
require 'json'
-VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/.freeze
-
-module GettextI18nRails
- class HamlParser
- singleton_class.send(:alias_method, :old_convert_to_code, :convert_to_code)
-
- # We need to convert text in Mustache format
- # to a format that can be parsed by Gettext scripts.
- # If we found a content like "{{ __('Stage') }}"
- # in a HAML file we convert it to "= _('Stage')", that way
- # it can be processed by the "rake gettext:find" script.
- #
- # Overwrites: https://github.com/grosser/gettext_i18n_rails/blob/8396387a431e0f8ead72fc1cd425cad2fa4992f2/lib/gettext_i18n_rails/haml_parser.rb#L9
- def self.convert_to_code(text)
- text.gsub!(VUE_TRANSLATE_REGEX, "\\2= \\3_(\\4)")
-
- old_convert_to_code(text)
- end
- end
-end
-
module GettextI18nRailsJs
module Parser
module Javascript
diff --git a/config/initializers/grape_validators.rb b/config/initializers/grape_validators.rb
index 07dd70822a..1492894e1f 100644
--- a/config/initializers/grape_validators.rb
+++ b/config/initializers/grape_validators.rb
@@ -10,3 +10,4 @@ Grape::Validations.register_validator(:check_assignees_count, ::API::Validations
Grape::Validations.register_validator(:untrusted_regexp, ::API::Validations::Validators::UntrustedRegexp)
Grape::Validations.register_validator(:email_or_email_list, ::API::Validations::Validators::EmailOrEmailList)
Grape::Validations.register_validator(:iteration_id, ::API::Validations::Validators::IntegerOrCustomValue)
+Grape::Validations.register_validator(:project_portable, ::API::Validations::Validators::ProjectPortable)
diff --git a/config/initializers/load_balancing.rb b/config/initializers/load_balancing.rb
index 2b58ae0f64..a31b11bb2b 100644
--- a/config/initializers/load_balancing.rb
+++ b/config/initializers/load_balancing.rb
@@ -1,28 +1,33 @@
# frozen_string_literal: true
-ActiveRecord::Base.singleton_class.attr_accessor :load_balancing_proxy
+Gitlab::Application.configure do |config|
+ config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
+end
-if Gitlab::Database::LoadBalancing.enable?
- Gitlab::Database.main.disable_prepared_statements
+Gitlab::Database::LoadBalancing.base_models.each do |model|
+ # The load balancer needs to be configured immediately, and re-configured
+ # after forking. This ensures queries that run before forking use the load
+ # balancer, and queries running after a fork don't run into any errors when
+ # using dead database connections.
+ #
+ # See https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63485 for more
+ # information.
+ Gitlab::Database::LoadBalancing::Setup.new(model).setup
- Gitlab::Application.configure do |config|
- config.middleware.use(Gitlab::Database::LoadBalancing::RackMiddleware)
+ # Database queries may be run before we fork, so we must set up the load
+ # balancer as early as possible. When we do fork, we need to make sure all the
+ # hosts are disconnected.
+ Gitlab::Cluster::LifecycleEvents.on_before_fork do
+ # When forking, we don't want to wait until the connections aren't in use
+ # any more, as this could delay the boot cycle.
+ model.connection.load_balancer.disconnect!(timeout: 0)
end
- # This hijacks the "connection" method to ensure both
- # `ActiveRecord::Base.connection` and all models use the same load
- # balancing proxy.
- ActiveRecord::Base.singleton_class.prepend(Gitlab::Database::LoadBalancing::ActiveRecordProxy)
-
- Gitlab::Database::LoadBalancing.configure_proxy
-
- # This needs to be executed after fork of clustered processes
+ # Service discovery only needs to run in the worker processes, as the main one
+ # won't be running many (if any) database queries.
Gitlab::Cluster::LifecycleEvents.on_worker_start do
- # For Host-based LB, we need to re-connect as Rails discards connections on fork
- Gitlab::Database::LoadBalancing.configure_proxy
-
- # Service discovery must be started after configuring the proxy, as service
- # discovery depends on this.
- Gitlab::Database::LoadBalancing.start_service_discovery
+ Gitlab::Database::LoadBalancing::Setup
+ .new(model, start_service_discovery: true)
+ .setup
end
end
diff --git a/config/initializers/mailer_retries.rb b/config/initializers/mailer_retries.rb
index 64fb0ffaa5..5980513af9 100644
--- a/config/initializers/mailer_retries.rb
+++ b/config/initializers/mailer_retries.rb
@@ -1,41 +1,5 @@
# frozen_string_literal: true
-class ActiveJob::QueueAdapters::SidekiqAdapter
- # With Sidekiq 6, we can do something like:
- # class ActionMailer::MailDeliveryJob
- # sidekiq_options retry: 3
- # end
- #
- # See https://gitlab.com/gitlab-org/gitlab/-/issues/329430
- raise "Update this monkey patch: #{__FILE__}" unless Sidekiq::VERSION == '5.2.9'
-
- def enqueue(job) #:nodoc:
- # Sidekiq::Client does not support symbols as keys
- job.provider_job_id = Sidekiq::Client.push \
- "class" => JobWrapper,
- "wrapped" => job.class.to_s,
- "queue" => job.queue_name,
- "args" => [job.serialize],
- "retry" => retry_for(job)
- end
-
- def enqueue_at(job, timestamp) #:nodoc:
- job.provider_job_id = Sidekiq::Client.push \
- "class" => JobWrapper,
- "wrapped" => job.class.to_s,
- "queue" => job.queue_name,
- "args" => [job.serialize],
- "at" => timestamp,
- "retry" => retry_for(job)
- end
-
- private
-
- def retry_for(job)
- if job.queue_name == 'mailers'
- 3
- else
- true
- end
- end
+Rails.application.config.after_initialize do
+ ActionMailer::MailDeliveryJob.sidekiq_options retry: 3
end
diff --git a/config/initializers/postgres_partitioning.rb b/config/initializers/postgres_partitioning.rb
index f2e2fba155..49f382547d 100644
--- a/config/initializers/postgres_partitioning.rb
+++ b/config/initializers/postgres_partitioning.rb
@@ -2,8 +2,7 @@
Gitlab::Database::Partitioning.register_models([
AuditEvent,
- WebHookLog,
- LooseForeignKeys::DeletedRecord
+ WebHookLog
])
if Gitlab.ee?
diff --git a/config/initializers/postgresql_cte.rb b/config/initializers/postgresql_cte.rb
index 6a9af7b486..7d00776e46 100644
--- a/config/initializers/postgresql_cte.rb
+++ b/config/initializers/postgresql_cte.rb
@@ -96,7 +96,7 @@ module ActiveRecord
end
end
- def build_arel(aliases)
+ def build_arel(aliases = nil)
arel = super
build_with(arel) if @values[:with]
diff --git a/config/initializers/sidekiq.rb b/config/initializers/sidekiq.rb
index 19c5e4df85..d33550b82d 100644
--- a/config/initializers/sidekiq.rb
+++ b/config/initializers/sidekiq.rb
@@ -27,8 +27,14 @@ use_sidekiq_daemon_memory_killer = ENV.fetch("SIDEKIQ_DAEMON_MEMORY_KILLER", 1).
use_sidekiq_legacy_memory_killer = !use_sidekiq_daemon_memory_killer
Sidekiq.configure_server do |config|
+ config.options[:strict] = false
+ config.options[:queues] = Gitlab::SidekiqConfig.expand_queues(config.options[:queues])
+ config.options[:scheduled_enq] = Gitlab::SidekiqEnq
+
+ Sidekiq.logger.info "Listening on queues #{config.options[:queues].uniq.sort}"
+
if enable_json_logs
- Sidekiq.logger.formatter = Gitlab::SidekiqLogging::JSONFormatter.new
+ config.log_formatter = Gitlab::SidekiqLogging::JSONFormatter.new
config.options[:job_logger] = Gitlab::SidekiqLogging::StructuredLogger
# Remove the default-provided handler. The exception is logged inside
@@ -38,11 +44,11 @@ Sidekiq.configure_server do |config|
config.redis = queues_config_hash
- config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator({
+ config.server_middleware(&Gitlab::SidekiqMiddleware.server_configurator(
metrics: Settings.monitoring.sidekiq_exporter,
arguments_logger: SidekiqLogArguments.enabled? && !enable_json_logs,
memory_killer: enable_sidekiq_memory_killer && use_sidekiq_legacy_memory_killer
- }))
+ ))
config.client_middleware(&Gitlab::SidekiqMiddleware.client_configurator)
diff --git a/config/initializers/zz_metrics.rb b/config/initializers/zz_metrics.rb
index 25e4ec0d48..8e69e1634f 100644
--- a/config/initializers/zz_metrics.rb
+++ b/config/initializers/zz_metrics.rb
@@ -3,128 +3,6 @@
# This file was prefixed with zz_ because we want to load it the last!
# See: https://gitlab.com/gitlab-org/gitlab-foss/issues/55611
-# Autoload all classes that we want to instrument, and instrument the methods we
-# need. This takes the Gitlab::Metrics::Instrumentation module as an argument so
-# that we can stub it for testing, as it is only called when metrics are
-# enabled.
-#
-# rubocop:disable Metrics/AbcSize
-def instrument_classes(instrumentation)
- return if ENV['STATIC_VERIFICATION']
-
- instrumentation.instrument_instance_methods(Gitlab::Shell)
-
- instrumentation.instrument_methods(Gitlab::Git)
-
- Gitlab::Git.constants.each do |name|
- const = Gitlab::Git.const_get(name, false)
-
- next unless const.is_a?(Module)
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
-
- # Path to search => prefix to strip from constant
- paths_to_instrument = {
- %w(app finders) => %w(app finders),
- %w(app mailers emails) => %w(app mailers),
- # Don't instrument `app/services/concerns`
- # It contains modules that are included in the services.
- # The services themselves are instrumented so the methods from the modules
- # are included.
- %w(app services [^concerns]**) => %w(app services),
- %w(lib gitlab conflicts) => ['lib'],
- %w(lib gitlab email message) => ['lib'],
- %w(lib gitlab checks) => ['lib']
- }
-
- paths_to_instrument.each do |(path, prefix)|
- prefix = Rails.root.join(*prefix)
-
- Dir[Rails.root.join(*path + ['*.rb'])].each do |file_path|
- path = Pathname.new(file_path).relative_path_from(prefix)
- const = path.to_s.sub('.rb', '').camelize.constantize
-
- instrumentation.instrument_methods(const)
- instrumentation.instrument_instance_methods(const)
- end
- end
-
- instrumentation.instrument_methods(Premailer::Adapter::Nokogiri)
- instrumentation.instrument_instance_methods(Premailer::Adapter::Nokogiri)
-
- instrumentation.instrument_methods(Banzai::Renderer)
- instrumentation.instrument_methods(Banzai::Querying)
-
- instrumentation.instrument_instance_methods(Banzai::ObjectRenderer)
- instrumentation.instrument_instance_methods(Banzai::ReferenceRedactor)
-
- [Issuable, Mentionable, Participable].each do |klass|
- instrumentation.instrument_instance_methods(klass)
- instrumentation.instrument_instance_methods(klass::ClassMethods)
- end
-
- instrumentation.instrument_methods(Gitlab::ReferenceExtractor)
- instrumentation.instrument_instance_methods(Gitlab::ReferenceExtractor)
-
- # Instrument the classes used for checking if somebody has push access.
- instrumentation.instrument_instance_methods(Gitlab::GitAccess)
- instrumentation.instrument_instance_methods(Gitlab::GitAccessWiki)
-
- instrumentation.instrument_instance_methods(API::Helpers)
-
- instrumentation.instrument_instance_methods(RepositoryCheck::SingleRepositoryWorker)
-
- instrumentation.instrument_instance_methods(Rouge::Formatters::HTMLGitlab)
-
- [:XML, :HTML].each do |namespace|
- namespace_mod = Nokogiri.const_get(namespace, false)
-
- instrumentation.instrument_methods(namespace_mod)
- instrumentation.instrument_methods(namespace_mod::Document)
- end
-
- instrumentation.instrument_methods(Rinku)
- instrumentation.instrument_instance_methods(Repository)
-
- instrumentation.instrument_methods(Gitlab::Highlight)
- instrumentation.instrument_instance_methods(Gitlab::Highlight)
- instrumentation.instrument_instance_method(Gitlab::Ci::Config::Yaml::Tags::Resolver, :to_hash)
-
- Gitlab.ee do
- instrumentation.instrument_instance_methods(Elastic::Latest::GitInstanceProxy)
- instrumentation.instrument_instance_methods(Elastic::Latest::GitClassProxy)
-
- instrumentation.instrument_instance_methods(Search::GlobalService)
- instrumentation.instrument_instance_methods(Search::ProjectService)
-
- instrumentation.instrument_instance_methods(Gitlab::Elastic::SearchResults)
- instrumentation.instrument_instance_methods(Gitlab::Elastic::ProjectSearchResults)
- instrumentation.instrument_instance_methods(Gitlab::Elastic::Indexer)
- instrumentation.instrument_instance_methods(Gitlab::Elastic::SnippetSearchResults)
- instrumentation.instrument_instance_methods(Gitlab::Elastic::Helper)
-
- instrumentation.instrument_instance_methods(Elastic::ApplicationVersionedSearch)
- instrumentation.instrument_instance_methods(Elastic::ProjectsSearch)
- instrumentation.instrument_instance_methods(Elastic::RepositoriesSearch)
- instrumentation.instrument_instance_methods(Elastic::SnippetsSearch)
- instrumentation.instrument_instance_methods(Elastic::WikiRepositoriesSearch)
-
- instrumentation.instrument_instance_methods(Gitlab::BitbucketImport::Importer)
- instrumentation.instrument_instance_methods(Bitbucket::Connection)
-
- instrumentation.instrument_instance_methods(Geo::RepositorySyncWorker)
- end
-
- # This is a Rails scope so we have to instrument it manually.
- instrumentation.instrument_method(Project, :visible_to_user)
-
- # Needed for https://gitlab.com/gitlab-org/gitlab-foss/issues/30224#note_32306159
- instrumentation.instrument_instance_method(MergeRequestDiff, :load_commits)
-end
-# rubocop:enable Metrics/AbcSize
-
# With prometheus enabled by default this breaks all specs
# that stubs methods using `any_instance_of` for the models reloaded here.
#
@@ -151,12 +29,8 @@ if Gitlab::Metrics.enabled? && !Rails.env.test? && !(Rails.env.development? && d
Gitlab::Application.configure do |config|
# We want to track certain metrics during the Load Balancing host resolving process.
# Because of that, we need to have metrics code available earlier for Load Balancing.
- if Gitlab::Database::LoadBalancing.enable?
- config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
- Gitlab::Metrics::RackMiddleware
- else
- config.middleware.use(Gitlab::Metrics::RackMiddleware)
- end
+ config.middleware.insert_before Gitlab::Database::LoadBalancing::RackMiddleware,
+ Gitlab::Metrics::RackMiddleware
config.middleware.use(Gitlab::Middleware::RailsQueueDuration)
config.middleware.use(Gitlab::Metrics::ElasticsearchRackMiddleware)
diff --git a/config/initializers_before_autoloader/002_sidekiq.rb b/config/initializers_before_autoloader/002_sidekiq.rb
index 8e2def0827..9ffcf39d6f 100644
--- a/config/initializers_before_autoloader/002_sidekiq.rb
+++ b/config/initializers_before_autoloader/002_sidekiq.rb
@@ -8,10 +8,6 @@
require 'sidekiq/web'
-# Disable the Sidekiq Rack session since GitLab already has its own session store.
-# CSRF protection still works (https://github.com/mperham/sidekiq/commit/315504e766c4fd88a29b7772169060afc4c40329).
-Sidekiq::Web.set :sessions, false
-
if Rails.env.development?
Sidekiq.default_worker_options[:backtrace] = true
end
diff --git a/config/initializers_before_autoloader/grape_entity_patch.rb b/config/initializers_before_autoloader/grape_entity_patch.rb
deleted file mode 100644
index 2db5876e75..0000000000
--- a/config/initializers_before_autoloader/grape_entity_patch.rb
+++ /dev/null
@@ -1,27 +0,0 @@
-# frozen_string_literal: true
-
-# This can be removed after the problem gets fixed on upstream.
-# You can follow https://github.com/ruby-grape/grape-entity/pull/355 to see the progress.
-#
-# For more information about the issue;
-# https://github.com/ruby/did_you_mean/issues/158#issuecomment-906056018
-
-require 'grape-entity'
-
-module Grape
- class Entity
- # Upstream version: https://github.com/ruby-grape/grape-entity/blob/675d3c0e20dfc1d6cf6f5ba5b46741bd404c8be7/lib/grape_entity/entity.rb#L520
- def exec_with_object(options, &block)
- if block.parameters.count == 1
- instance_exec(object, &block)
- else
- instance_exec(object, options, &block)
- end
- rescue StandardError => e
- # it handles: https://github.com/ruby/ruby/blob/v3_0_0_preview1/NEWS.md#language-changes point 3, Proc
- raise Grape::Entity::Deprecated.new e.message, 'in ruby 3.0' if e.is_a?(ArgumentError)
-
- raise e
- end
- end
-end
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 4615c9a739..233dca33bb 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -10,6 +10,9 @@ en:
target: Target issue
group:
path: Group URL
+ member:
+ user: "The member's email address"
+ invite_email: "The member's email address"
project/error_tracking_setting:
token: "Auth Token"
project: "Project"
diff --git a/config/mail_room.yml b/config/mail_room.yml
index 25bda294a1..895438dcc4 100644
--- a/config/mail_room.yml
+++ b/config/mail_room.yml
@@ -29,6 +29,7 @@
:delivery_method: sidekiq
:delivery_options:
:redis_url: <%= config[:redis_url].to_json %>
+ :redis_db: <%= config[:redis_db] %>
:namespace: <%= Gitlab::Redis::Queues::SIDEKIQ_NAMESPACE %>
:queue: <%= config[:queue] %>
:worker: <%= config[:worker] %>
diff --git a/config/metrics/counts_28d/20210216175055_merge_requests.yml b/config/metrics/counts_28d/20210216175055_merge_requests.yml
index dca4f81eee..06c4696495 100644
--- a/config/metrics/counts_28d/20210216175055_merge_requests.yml
+++ b/config/metrics/counts_28d/20210216175055_merge_requests.yml
@@ -1,7 +1,7 @@
---
data_category: optional
key_path: usage_activity_by_stage_monthly.create.merge_requests
-description: Count of the number of users creating merge requests
+description: Count distinct authors of merge requests
product_section: dev
product_stage: create
product_group: group::code review
diff --git a/config/metrics/counts_28d/20210216175101_merge_requests_users.yml b/config/metrics/counts_28d/20210216175101_merge_requests_users.yml
index 04f0f124e8..2b43fbf813 100644
--- a/config/metrics/counts_28d/20210216175101_merge_requests_users.yml
+++ b/config/metrics/counts_28d/20210216175101_merge_requests_users.yml
@@ -1,7 +1,7 @@
---
data_category: optional
key_path: usage_activity_by_stage_monthly.create.merge_requests_users
-description: Monthly count of the number of merge request users
+description: Distinct count of users performing merge request actions like closed, merged, created, commented
product_section: dev
product_stage: create
product_group: group::code review
diff --git a/config/metrics/counts_28d/20210216175405_clusters_applications_cert_managers.yml b/config/metrics/counts_28d/20210216175405_clusters_applications_cert_managers.yml
index a34c679e91..76627a91d0 100644
--- a/config/metrics/counts_28d/20210216175405_clusters_applications_cert_managers.yml
+++ b/config/metrics/counts_28d/20210216175405_clusters_applications_cert_managers.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -20,3 +20,4 @@ tier:
name: 'count_distinct_user_id_from_clusters_applications_cert_managers'
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210216175407_clusters_applications_helm.yml b/config/metrics/counts_28d/20210216175407_clusters_applications_helm.yml
index 2cc0e937d9..ced2b45484 100644
--- a/config/metrics/counts_28d/20210216175407_clusters_applications_helm.yml
+++ b/config/metrics/counts_28d/20210216175407_clusters_applications_helm.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -20,3 +20,4 @@ tier:
name: 'count_distinct_user_id_from_clusters_applications_helm'
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210216175409_clusters_applications_ingress.yml b/config/metrics/counts_28d/20210216175409_clusters_applications_ingress.yml
index 2cdd680eae..eee5839794 100644
--- a/config/metrics/counts_28d/20210216175409_clusters_applications_ingress.yml
+++ b/config/metrics/counts_28d/20210216175409_clusters_applications_ingress.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -20,3 +20,4 @@ tier:
name: 'count_distinct_user_id_from_clusters_applications_ingress'
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210216175411_clusters_applications_knative.yml b/config/metrics/counts_28d/20210216175411_clusters_applications_knative.yml
index 23114b91d1..f12fe19122 100644
--- a/config/metrics/counts_28d/20210216175411_clusters_applications_knative.yml
+++ b/config/metrics/counts_28d/20210216175411_clusters_applications_knative.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -20,3 +20,4 @@ tier:
name: 'count_distinct_user_id_from_clusters_applications_knative'
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210216180958_clusters_applications_prometheus.yml b/config/metrics/counts_28d/20210216180958_clusters_applications_prometheus.yml
index b947a6ff7e..895746f512 100644
--- a/config/metrics/counts_28d/20210216180958_clusters_applications_prometheus.yml
+++ b/config/metrics/counts_28d/20210216180958_clusters_applications_prometheus.yml
@@ -7,7 +7,7 @@ product_stage: monitor
product_group: group::monitor
product_category: metrics
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210216181951_clusters_applications_runner.yml b/config/metrics/counts_28d/20210216181951_clusters_applications_runner.yml
index 22702146bc..4d676e436b 100644
--- a/config/metrics/counts_28d/20210216181951_clusters_applications_runner.yml
+++ b/config/metrics/counts_28d/20210216181951_clusters_applications_runner.yml
@@ -7,7 +7,7 @@ product_stage: verify
product_group: group::runner
product_category: runner
value_type: number
-status: active
+status: removed
time_frame: 28d
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_28d/20210715094458_releases_with_milestones.yml b/config/metrics/counts_28d/20210715094458_releases_with_milestones.yml
new file mode 100644
index 0000000000..74f9406a13
--- /dev/null
+++ b/config/metrics/counts_28d/20210715094458_releases_with_milestones.yml
@@ -0,0 +1,23 @@
+---
+key_path: usage_activity_by_stage_monthly.release.releases_with_milestones
+description: Unique users creating releases with milestones associated
+performance_indicator_type: [smau]
+product_section: ops
+product_stage: release
+product_group: 'group::release'
+product_category: Release Orchestration
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71287'
+time_frame: 28d
+data_source: database
+instrumentation_class: 'CountUsersAssociatingMilestonesToReleasesMetric'
+data_category: Optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_28d/20210916080405_promoted_issues.yml b/config/metrics/counts_28d/20210916080405_promoted_issues.yml
index 106c3a6128..ec73682b1f 100644
--- a/config/metrics/counts_28d/20210916080405_promoted_issues.yml
+++ b/config/metrics/counts_28d/20210916080405_promoted_issues.yml
@@ -7,7 +7,7 @@ product_stage: growth
product_group: group::product intelligence
product_category: collection
value_type: number
-status: active
+status: deprecated
milestone: "14.3"
introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70485
time_frame: 28d
diff --git a/config/metrics/counts_28d/20210916201533_clusters_integrations_prometheus.yml b/config/metrics/counts_28d/20210916201533_clusters_integrations_prometheus.yml
new file mode 100644
index 0000000000..6ecc98dcad
--- /dev/null
+++ b/config/metrics/counts_28d/20210916201533_clusters_integrations_prometheus.yml
@@ -0,0 +1,21 @@
+---
+data_category: optional
+key_path: usage_activity_by_stage_monthly.monitor.clusters_integrations_prometheus
+description: Users creating clusters with Prometheus integration enabled in last 28 days.
+product_section: ops
+product_stage: monitor
+product_group: group::monitor
+product_category: metrics
+value_type: number
+status: active
+time_frame: 28d
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/metrics/counts_28d/20210929102434_p_ci_templates_implicit_jobs_build_monthly.yml b/config/metrics/counts_28d/20210929102434_p_ci_templates_implicit_jobs_build_monthly.yml
new file mode 100644
index 0000000000..4fa6d3d884
--- /dev/null
+++ b/config/metrics/counts_28d/20210929102434_p_ci_templates_implicit_jobs_build_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_build_monthly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_build
diff --git a/config/metrics/counts_28d/20210929102736_p_ci_templates_implicit_jobs_deploy_latest_monthly.yml b/config/metrics/counts_28d/20210929102736_p_ci_templates_implicit_jobs_deploy_latest_monthly.yml
new file mode 100644
index 0000000000..9d19a6ffa7
--- /dev/null
+++ b/config/metrics/counts_28d/20210929102736_p_ci_templates_implicit_jobs_deploy_latest_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_latest_monthly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_deploy_latest
diff --git a/config/metrics/counts_28d/20210929103010_p_ci_templates_implicit_jobs_deploy_monthly.yml b/config/metrics/counts_28d/20210929103010_p_ci_templates_implicit_jobs_deploy_monthly.yml
new file mode 100644
index 0000000000..5b7b7924c4
--- /dev/null
+++ b/config/metrics/counts_28d/20210929103010_p_ci_templates_implicit_jobs_deploy_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_monthly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_deploy
diff --git a/config/metrics/counts_28d/20210930125418_github_import_project_start_monthly.yml b/config/metrics/counts_28d/20210930125418_github_import_project_start_monthly.yml
new file mode 100644
index 0000000000..2812aa73ca
--- /dev/null
+++ b/config/metrics/counts_28d/20210930125418_github_import_project_start_monthly.yml
@@ -0,0 +1,26 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_start_monthly
+description: The number of github projects that were enqueued to start monthy
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_start
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_28d/20210930130531_github_import_project_success_monthly.yml b/config/metrics/counts_28d/20210930130531_github_import_project_success_monthly.yml
new file mode 100644
index 0000000000..ab599c6737
--- /dev/null
+++ b/config/metrics/counts_28d/20210930130531_github_import_project_success_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_success_monthly
+description: The number of github projects that were successful monthly
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_success
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_28d/20210930163813_github_import_project_failure_monthly.yml b/config/metrics/counts_28d/20210930163813_github_import_project_failure_monthly.yml
new file mode 100644
index 0000000000..6651a77092
--- /dev/null
+++ b/config/metrics/counts_28d/20210930163813_github_import_project_failure_monthly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_failure_monthly
+description: The number of github projects that failed monthly
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 28d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_failure
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210916100524_groups_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210916100524_groups_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..9a23d73b13
--- /dev/null
+++ b/config/metrics/counts_7d/20210916100524_groups_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.groups_gitlab_slack_application_active
+name: count_groups_gitlab_slack_application_active
+description: Count groups with active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210916101641_projects_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210916101641_projects_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..2d1124a597
--- /dev/null
+++ b/config/metrics/counts_7d/20210916101641_projects_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.projects_gitlab_slack_application_active
+name: count_project_gitlab_slack_application_active
+description: Count projects with active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210916101837_instances_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210916101837_instances_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..8a04e024eb
--- /dev/null
+++ b/config/metrics/counts_7d/20210916101837_instances_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.instances_gitlab_slack_application_active
+name: count_instances_gitlab_slack_application_active
+description: Count instances with active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gilab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210916102312_templates_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210916102312_templates_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..d465e1b3d0
--- /dev/null
+++ b/config/metrics/counts_7d/20210916102312_templates_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.templates_gitlab_slack_application_active
+name: count_templates_gitlab_slack_application_active
+description: Count templates with active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210917040700_groups_inheriting_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210917040700_groups_inheriting_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..2a62c45c54
--- /dev/null
+++ b/config/metrics/counts_7d/20210917040700_groups_inheriting_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.groups_inheriting_gitlab_slack_application_active
+name: count_groups_inheriting_gitlab_slack_application_active
+description: Count groups inheriting active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210917040956_projects_inheriting_gitlab_slack_application_active.yml b/config/metrics/counts_7d/20210917040956_projects_inheriting_gitlab_slack_application_active.yml
new file mode 100644
index 0000000000..266670159b
--- /dev/null
+++ b/config/metrics/counts_7d/20210917040956_projects_inheriting_gitlab_slack_application_active.yml
@@ -0,0 +1,23 @@
+---
+key_path: counts.projects_inheriting_gitlab_slack_application_active
+name: count_project_inheriting_gitlab_slack_application_active
+description: Count projects inheriting active slack application
+product_section: dev
+product_stage: ecosystem
+product_group: group::integrations
+product_category: integrations
+value_type: number
+status: active
+milestone: "14.3"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70496
+time_frame: 7d
+data_source: database
+data_category: optional
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210929102429_p_ci_templates_implicit_jobs_build_weekly.yml b/config/metrics/counts_7d/20210929102429_p_ci_templates_implicit_jobs_build_weekly.yml
new file mode 100644
index 0000000000..f13174a4af
--- /dev/null
+++ b/config/metrics/counts_7d/20210929102429_p_ci_templates_implicit_jobs_build_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_build_weekly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_build
diff --git a/config/metrics/counts_7d/20210929102731_p_ci_templates_implicit_jobs_deploy_latest_weekly.yml b/config/metrics/counts_7d/20210929102731_p_ci_templates_implicit_jobs_deploy_latest_weekly.yml
new file mode 100644
index 0000000000..f4d0ac7ff9
--- /dev/null
+++ b/config/metrics/counts_7d/20210929102731_p_ci_templates_implicit_jobs_deploy_latest_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_latest_weekly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_deploy_latest
diff --git a/config/metrics/counts_7d/20210929103006_p_ci_templates_implicit_jobs_deploy_weekly.yml b/config/metrics/counts_7d/20210929103006_p_ci_templates_implicit_jobs_deploy_weekly.yml
new file mode 100644
index 0000000000..964b57c65b
--- /dev/null
+++ b/config/metrics/counts_7d/20210929103006_p_ci_templates_implicit_jobs_deploy_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.ci_templates.p_ci_templates_implicit_jobs_deploy_weekly
+description: ''
+product_section: ''
+product_stage: ''
+product_group: ''
+product_category: ''
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71157
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+options:
+ events:
+ - p_ci_templates_implicit_jobs_deploy
diff --git a/config/metrics/counts_7d/20210930125411_github_import_project_start_weekly.yml b/config/metrics/counts_7d/20210930125411_github_import_project_start_weekly.yml
new file mode 100644
index 0000000000..2c5b7d46e1
--- /dev/null
+++ b/config/metrics/counts_7d/20210930125411_github_import_project_start_weekly.yml
@@ -0,0 +1,26 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_start_weekly
+description: The number of github projects that were enqueued to start weekly
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_start
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210930130525_github_import_project_success_weekly.yml b/config/metrics/counts_7d/20210930130525_github_import_project_success_weekly.yml
new file mode 100644
index 0000000000..10147658dd
--- /dev/null
+++ b/config/metrics/counts_7d/20210930130525_github_import_project_success_weekly.yml
@@ -0,0 +1,26 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_success_weekly
+description: The number of github projects that were successful weekly
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_success
+performance_indicator_type: []
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_7d/20210930163807_github_import_project_failure_weekly.yml b/config/metrics/counts_7d/20210930163807_github_import_project_failure_weekly.yml
new file mode 100644
index 0000000000..33a1902504
--- /dev/null
+++ b/config/metrics/counts_7d/20210930163807_github_import_project_failure_weekly.yml
@@ -0,0 +1,25 @@
+---
+key_path: redis_hll_counters.importer.github_import_project_failure_weekly
+description: The number of github projects that failed weekly
+product_section: dev
+product_stage: devops
+product_group: group::import
+product_category:
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: https://gitlab.com/gitlab-org/gitlab/-/merge_requests/70012
+time_frame: 7d
+data_source: redis_hll
+data_category: optional
+instrumentation_class: RedisHLLMetric
+options:
+ events:
+ - github_import_project_failure
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210216175255_clusters_applications_helm.yml b/config/metrics/counts_all/20210216175255_clusters_applications_helm.yml
index 661d809ae6..e772f66951 100644
--- a/config/metrics/counts_all/20210216175255_clusters_applications_helm.yml
+++ b/config/metrics/counts_all/20210216175255_clusters_applications_helm.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175257_clusters_applications_ingress.yml b/config/metrics/counts_all/20210216175257_clusters_applications_ingress.yml
index 889e701a8d..1e31c28f54 100644
--- a/config/metrics/counts_all/20210216175257_clusters_applications_ingress.yml
+++ b/config/metrics/counts_all/20210216175257_clusters_applications_ingress.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175259_clusters_applications_cert_managers.yml b/config/metrics/counts_all/20210216175259_clusters_applications_cert_managers.yml
index a22cf9dd84..bc16040f14 100644
--- a/config/metrics/counts_all/20210216175259_clusters_applications_cert_managers.yml
+++ b/config/metrics/counts_all/20210216175259_clusters_applications_cert_managers.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175301_clusters_applications_crossplane.yml b/config/metrics/counts_all/20210216175301_clusters_applications_crossplane.yml
index 1fc50c89bd..2856a0e25a 100644
--- a/config/metrics/counts_all/20210216175301_clusters_applications_crossplane.yml
+++ b/config/metrics/counts_all/20210216175301_clusters_applications_crossplane.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175303_clusters_applications_prometheus.yml b/config/metrics/counts_all/20210216175303_clusters_applications_prometheus.yml
index b883f9cdd1..961c9adfdb 100644
--- a/config/metrics/counts_all/20210216175303_clusters_applications_prometheus.yml
+++ b/config/metrics/counts_all/20210216175303_clusters_applications_prometheus.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175305_clusters_applications_runner.yml b/config/metrics/counts_all/20210216175305_clusters_applications_runner.yml
index 5f9c9a9c0d..33f8d045d2 100644
--- a/config/metrics/counts_all/20210216175305_clusters_applications_runner.yml
+++ b/config/metrics/counts_all/20210216175305_clusters_applications_runner.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175307_clusters_applications_knative.yml b/config/metrics/counts_all/20210216175307_clusters_applications_knative.yml
index 8b5f50c6c7..e5a80ca638 100644
--- a/config/metrics/counts_all/20210216175307_clusters_applications_knative.yml
+++ b/config/metrics/counts_all/20210216175307_clusters_applications_knative.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175309_clusters_applications_elastic_stack.yml b/config/metrics/counts_all/20210216175309_clusters_applications_elastic_stack.yml
index fafbf8a7d4..4d5dbcd198 100644
--- a/config/metrics/counts_all/20210216175309_clusters_applications_elastic_stack.yml
+++ b/config/metrics/counts_all/20210216175309_clusters_applications_elastic_stack.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175310_clusters_applications_jupyter.yml b/config/metrics/counts_all/20210216175310_clusters_applications_jupyter.yml
index 89c8116f0d..85d62985fa 100644
--- a/config/metrics/counts_all/20210216175310_clusters_applications_jupyter.yml
+++ b/config/metrics/counts_all/20210216175310_clusters_applications_jupyter.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175312_clusters_applications_cilium.yml b/config/metrics/counts_all/20210216175312_clusters_applications_cilium.yml
index faf7f486d3..33a8a53a8a 100644
--- a/config/metrics/counts_all/20210216175312_clusters_applications_cilium.yml
+++ b/config/metrics/counts_all/20210216175312_clusters_applications_cilium.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -19,3 +19,4 @@ tier:
- ultimate
performance_indicator_type: []
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175329_clusters_applications_cert_managers.yml b/config/metrics/counts_all/20210216175329_clusters_applications_cert_managers.yml
index 185ab921fd..f97816e18c 100644
--- a/config/metrics/counts_all/20210216175329_clusters_applications_cert_managers.yml
+++ b/config/metrics/counts_all/20210216175329_clusters_applications_cert_managers.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175331_clusters_applications_helm.yml b/config/metrics/counts_all/20210216175331_clusters_applications_helm.yml
index 4561338a3c..28c58e1185 100644
--- a/config/metrics/counts_all/20210216175331_clusters_applications_helm.yml
+++ b/config/metrics/counts_all/20210216175331_clusters_applications_helm.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175333_clusters_applications_ingress.yml b/config/metrics/counts_all/20210216175333_clusters_applications_ingress.yml
index 8336b6a404..b93198b92a 100644
--- a/config/metrics/counts_all/20210216175333_clusters_applications_ingress.yml
+++ b/config/metrics/counts_all/20210216175333_clusters_applications_ingress.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175335_clusters_applications_knative.yml b/config/metrics/counts_all/20210216175335_clusters_applications_knative.yml
index c5979093a1..735f3ea8e0 100644
--- a/config/metrics/counts_all/20210216175335_clusters_applications_knative.yml
+++ b/config/metrics/counts_all/20210216175335_clusters_applications_knative.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216175627_templates_asana_active.yml b/config/metrics/counts_all/20210216175627_templates_asana_active.yml
index 48025516d3..3575f359f9 100644
--- a/config/metrics/counts_all/20210216175627_templates_asana_active.yml
+++ b/config/metrics/counts_all/20210216175627_templates_asana_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175638_templates_assembla_active.yml b/config/metrics/counts_all/20210216175638_templates_assembla_active.yml
index 1ab0d42a5e..d0f203e12c 100644
--- a/config/metrics/counts_all/20210216175638_templates_assembla_active.yml
+++ b/config/metrics/counts_all/20210216175638_templates_assembla_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175649_templates_bamboo_active.yml b/config/metrics/counts_all/20210216175649_templates_bamboo_active.yml
index 85b10732ae..2e97c4bf1b 100644
--- a/config/metrics/counts_all/20210216175649_templates_bamboo_active.yml
+++ b/config/metrics/counts_all/20210216175649_templates_bamboo_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175701_templates_bugzilla_active.yml b/config/metrics/counts_all/20210216175701_templates_bugzilla_active.yml
index bc5a123801..6d26fbfb8d 100644
--- a/config/metrics/counts_all/20210216175701_templates_bugzilla_active.yml
+++ b/config/metrics/counts_all/20210216175701_templates_bugzilla_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175712_templates_buildkite_active.yml b/config/metrics/counts_all/20210216175712_templates_buildkite_active.yml
index 16287a4e71..8218ae10ca 100644
--- a/config/metrics/counts_all/20210216175712_templates_buildkite_active.yml
+++ b/config/metrics/counts_all/20210216175712_templates_buildkite_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175723_templates_campfire_active.yml b/config/metrics/counts_all/20210216175723_templates_campfire_active.yml
index 728d3d0af4..2e8b09e499 100644
--- a/config/metrics/counts_all/20210216175723_templates_campfire_active.yml
+++ b/config/metrics/counts_all/20210216175723_templates_campfire_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175734_templates_confluence_active.yml b/config/metrics/counts_all/20210216175734_templates_confluence_active.yml
index bb2d997611..2287415e83 100644
--- a/config/metrics/counts_all/20210216175734_templates_confluence_active.yml
+++ b/config/metrics/counts_all/20210216175734_templates_confluence_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175745_templates_custom_issue_tracker_active.yml b/config/metrics/counts_all/20210216175745_templates_custom_issue_tracker_active.yml
index c1c63d0529..343c4c887a 100644
--- a/config/metrics/counts_all/20210216175745_templates_custom_issue_tracker_active.yml
+++ b/config/metrics/counts_all/20210216175745_templates_custom_issue_tracker_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175756_templates_discord_active.yml b/config/metrics/counts_all/20210216175756_templates_discord_active.yml
index d0663da787..9f057665e3 100644
--- a/config/metrics/counts_all/20210216175756_templates_discord_active.yml
+++ b/config/metrics/counts_all/20210216175756_templates_discord_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175807_templates_drone_ci_active.yml b/config/metrics/counts_all/20210216175807_templates_drone_ci_active.yml
index 6a764f64ab..edec715a03 100644
--- a/config/metrics/counts_all/20210216175807_templates_drone_ci_active.yml
+++ b/config/metrics/counts_all/20210216175807_templates_drone_ci_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175818_templates_emails_on_push_active.yml b/config/metrics/counts_all/20210216175818_templates_emails_on_push_active.yml
index f709aff9f0..057e3b3056 100644
--- a/config/metrics/counts_all/20210216175818_templates_emails_on_push_active.yml
+++ b/config/metrics/counts_all/20210216175818_templates_emails_on_push_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175829_templates_external_wiki_active.yml b/config/metrics/counts_all/20210216175829_templates_external_wiki_active.yml
index 35bbce9538..31a270395f 100644
--- a/config/metrics/counts_all/20210216175829_templates_external_wiki_active.yml
+++ b/config/metrics/counts_all/20210216175829_templates_external_wiki_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175840_templates_flowdock_active.yml b/config/metrics/counts_all/20210216175840_templates_flowdock_active.yml
index dc1466d2be..74d085a444 100644
--- a/config/metrics/counts_all/20210216175840_templates_flowdock_active.yml
+++ b/config/metrics/counts_all/20210216175840_templates_flowdock_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175902_templates_hangouts_chat_active.yml b/config/metrics/counts_all/20210216175902_templates_hangouts_chat_active.yml
index b676501db1..fb007335b2 100644
--- a/config/metrics/counts_all/20210216175902_templates_hangouts_chat_active.yml
+++ b/config/metrics/counts_all/20210216175902_templates_hangouts_chat_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175924_templates_irker_active.yml b/config/metrics/counts_all/20210216175924_templates_irker_active.yml
index fc3ed89b6e..c3491169a7 100644
--- a/config/metrics/counts_all/20210216175924_templates_irker_active.yml
+++ b/config/metrics/counts_all/20210216175924_templates_irker_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175935_templates_jenkins_active.yml b/config/metrics/counts_all/20210216175935_templates_jenkins_active.yml
index 542671304b..96938d84ae 100644
--- a/config/metrics/counts_all/20210216175935_templates_jenkins_active.yml
+++ b/config/metrics/counts_all/20210216175935_templates_jenkins_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175946_templates_jira_active.yml b/config/metrics/counts_all/20210216175946_templates_jira_active.yml
index 83d3669b7f..964aef061d 100644
--- a/config/metrics/counts_all/20210216175946_templates_jira_active.yml
+++ b/config/metrics/counts_all/20210216175946_templates_jira_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216175957_templates_mattermost_active.yml b/config/metrics/counts_all/20210216175957_templates_mattermost_active.yml
index 460c5e808d..9e472a1cb5 100644
--- a/config/metrics/counts_all/20210216175957_templates_mattermost_active.yml
+++ b/config/metrics/counts_all/20210216175957_templates_mattermost_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180008_templates_mattermost_slash_commands_active.yml b/config/metrics/counts_all/20210216180008_templates_mattermost_slash_commands_active.yml
index 927710d918..97a8dccd83 100644
--- a/config/metrics/counts_all/20210216180008_templates_mattermost_slash_commands_active.yml
+++ b/config/metrics/counts_all/20210216180008_templates_mattermost_slash_commands_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180019_templates_microsoft_teams_active.yml b/config/metrics/counts_all/20210216180019_templates_microsoft_teams_active.yml
index fb35802c2b..1fed89e793 100644
--- a/config/metrics/counts_all/20210216180019_templates_microsoft_teams_active.yml
+++ b/config/metrics/counts_all/20210216180019_templates_microsoft_teams_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180030_templates_packagist_active.yml b/config/metrics/counts_all/20210216180030_templates_packagist_active.yml
index aa56cf0802..75c4f0f242 100644
--- a/config/metrics/counts_all/20210216180030_templates_packagist_active.yml
+++ b/config/metrics/counts_all/20210216180030_templates_packagist_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180041_templates_pipelines_email_active.yml b/config/metrics/counts_all/20210216180041_templates_pipelines_email_active.yml
index 2d81cf582e..7462974968 100644
--- a/config/metrics/counts_all/20210216180041_templates_pipelines_email_active.yml
+++ b/config/metrics/counts_all/20210216180041_templates_pipelines_email_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180052_templates_pivotaltracker_active.yml b/config/metrics/counts_all/20210216180052_templates_pivotaltracker_active.yml
index 7c869361d0..abc6227f43 100644
--- a/config/metrics/counts_all/20210216180052_templates_pivotaltracker_active.yml
+++ b/config/metrics/counts_all/20210216180052_templates_pivotaltracker_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180104_templates_pushover_active.yml b/config/metrics/counts_all/20210216180104_templates_pushover_active.yml
index 572b5da07d..9580fe0106 100644
--- a/config/metrics/counts_all/20210216180104_templates_pushover_active.yml
+++ b/config/metrics/counts_all/20210216180104_templates_pushover_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180115_templates_redmine_active.yml b/config/metrics/counts_all/20210216180115_templates_redmine_active.yml
index a85417cbc2..47ba5ed946 100644
--- a/config/metrics/counts_all/20210216180115_templates_redmine_active.yml
+++ b/config/metrics/counts_all/20210216180115_templates_redmine_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180126_templates_slack_active.yml b/config/metrics/counts_all/20210216180126_templates_slack_active.yml
index dffa673cd4..792c965216 100644
--- a/config/metrics/counts_all/20210216180126_templates_slack_active.yml
+++ b/config/metrics/counts_all/20210216180126_templates_slack_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180137_templates_slack_slash_commands_active.yml b/config/metrics/counts_all/20210216180137_templates_slack_slash_commands_active.yml
index e044d6cf1d..38bc695931 100644
--- a/config/metrics/counts_all/20210216180137_templates_slack_slash_commands_active.yml
+++ b/config/metrics/counts_all/20210216180137_templates_slack_slash_commands_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180148_templates_teamcity_active.yml b/config/metrics/counts_all/20210216180148_templates_teamcity_active.yml
index b3a37ebc57..63e8285e22 100644
--- a/config/metrics/counts_all/20210216180148_templates_teamcity_active.yml
+++ b/config/metrics/counts_all/20210216180148_templates_teamcity_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180159_templates_unify_circuit_active.yml b/config/metrics/counts_all/20210216180159_templates_unify_circuit_active.yml
index 39facfb237..10c7b23a30 100644
--- a/config/metrics/counts_all/20210216180159_templates_unify_circuit_active.yml
+++ b/config/metrics/counts_all/20210216180159_templates_unify_circuit_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180210_templates_webex_teams_active.yml b/config/metrics/counts_all/20210216180210_templates_webex_teams_active.yml
index 185b538a22..1e41a8170d 100644
--- a/config/metrics/counts_all/20210216180210_templates_webex_teams_active.yml
+++ b/config/metrics/counts_all/20210216180210_templates_webex_teams_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180221_templates_youtrack_active.yml b/config/metrics/counts_all/20210216180221_templates_youtrack_active.yml
index a6c11918ce..edd5aa07f5 100644
--- a/config/metrics/counts_all/20210216180221_templates_youtrack_active.yml
+++ b/config/metrics/counts_all/20210216180221_templates_youtrack_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180934_templates_prometheus_active.yml b/config/metrics/counts_all/20210216180934_templates_prometheus_active.yml
index 4bf8911c66..9f8a24488a 100644
--- a/config/metrics/counts_all/20210216180934_templates_prometheus_active.yml
+++ b/config/metrics/counts_all/20210216180934_templates_prometheus_active.yml
@@ -7,7 +7,8 @@ product_stage: monitor
product_group: group::monitor
product_category: metrics
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216180947_clusters_applications_prometheus.yml b/config/metrics/counts_all/20210216180947_clusters_applications_prometheus.yml
index a43dc103f9..bf56733738 100644
--- a/config/metrics/counts_all/20210216180947_clusters_applications_prometheus.yml
+++ b/config/metrics/counts_all/20210216180947_clusters_applications_prometheus.yml
@@ -7,7 +7,7 @@ product_stage: monitor
product_group: group::monitor
product_category: metrics
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216181949_clusters_applications_runner.yml b/config/metrics/counts_all/20210216181949_clusters_applications_runner.yml
index 96f16043e6..3c3fc60069 100644
--- a/config/metrics/counts_all/20210216181949_clusters_applications_runner.yml
+++ b/config/metrics/counts_all/20210216181949_clusters_applications_runner.yml
@@ -7,7 +7,7 @@ product_stage: configure
product_group: group::configure
product_category: kubernetes_management
value_type: number
-status: active
+status: removed
time_frame: all
data_source: database
distribution:
@@ -18,3 +18,4 @@ tier:
- premium
- ultimate
milestone: "<13.9"
+milestone_removed: "14.4"
diff --git a/config/metrics/counts_all/20210216182112_sast_jobs.yml b/config/metrics/counts_all/20210216182112_sast_jobs.yml
deleted file mode 100644
index 1012910675..0000000000
--- a/config/metrics/counts_all/20210216182112_sast_jobs.yml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-data_category: operational
-key_path: counts.sast_jobs
-description: Count of SAST CI jobs for the month. Job names ending in '-sast'
-product_section: sec
-product_stage: secure
-product_group: group::static analysis
-product_category: static_application_security_testing
-value_type: number
-status: active
-time_frame: all
-data_source: database
-distribution:
-- ce
-- ee
-tier:
-- free
-- premium
-- ultimate
-performance_indicator_type: []
-milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml b/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml
deleted file mode 100644
index 8a3d1ef15f..0000000000
--- a/config/metrics/counts_all/20210216182114_secret_detection_jobs.yml
+++ /dev/null
@@ -1,21 +0,0 @@
----
-data_category: operational
-key_path: counts.secret_detection_jobs
-description: Count of all 'secret-detection' CI jobs.
-product_section: sec
-product_stage: secure
-product_group: group::static analysis
-product_category: secret_detection
-value_type: number
-status: active
-time_frame: all
-data_source: database
-distribution:
-- ce
-- ee
-tier:
-- free
-- premium
-- ultimate
-performance_indicator_type: []
-milestone: "<13.9"
diff --git a/config/metrics/counts_all/20210216182551_templates_datadog_active.yml b/config/metrics/counts_all/20210216182551_templates_datadog_active.yml
index d2df07b8d2..975bc2164b 100644
--- a/config/metrics/counts_all/20210216182551_templates_datadog_active.yml
+++ b/config/metrics/counts_all/20210216182551_templates_datadog_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216182618_templates_ewm_active.yml b/config/metrics/counts_all/20210216182618_templates_ewm_active.yml
index 1ace369980..be61177d81 100644
--- a/config/metrics/counts_all/20210216182618_templates_ewm_active.yml
+++ b/config/metrics/counts_all/20210216182618_templates_ewm_active.yml
@@ -7,7 +7,8 @@ product_stage: ecosystem
product_group: group::integrations
product_category: integrations
value_type: number
-status: active
+status: removed
+milestone_removed: '14.4'
time_frame: all
data_source: database
distribution:
diff --git a/config/metrics/counts_all/20210216182907_package_events_i_package_container_delete_package.yml b/config/metrics/counts_all/20210216182907_package_events_i_package_container_delete_package.yml
index e36411a81e..c20a79a2f2 100644
--- a/config/metrics/counts_all/20210216182907_package_events_i_package_container_delete_package.yml
+++ b/config/metrics/counts_all/20210216182907_package_events_i_package_container_delete_package.yml
@@ -7,7 +7,8 @@ product_stage: package
product_group: group::package
product_category: container registry
value_type: number
-status: deprecated
+status: removed
+milestone_removed: "14.4"
time_frame: all
data_source: redis
distribution:
diff --git a/config/metrics/counts_all/20210216182909_package_events_i_package_container_pull_package.yml b/config/metrics/counts_all/20210216182909_package_events_i_package_container_pull_package.yml
index c9c448b642..ff3ab7b40c 100644
--- a/config/metrics/counts_all/20210216182909_package_events_i_package_container_pull_package.yml
+++ b/config/metrics/counts_all/20210216182909_package_events_i_package_container_pull_package.yml
@@ -7,7 +7,8 @@ product_stage: package
product_group: group::package
product_category: container registry
value_type: number
-status: deprecated
+status: removed
+milestone_removed: "14.4"
time_frame: all
data_source: redis
distribution:
diff --git a/config/metrics/counts_all/20210216182911_package_events_i_package_container_push_package.yml b/config/metrics/counts_all/20210216182911_package_events_i_package_container_push_package.yml
index 9ca8e62f68..2c9d9a391a 100644
--- a/config/metrics/counts_all/20210216182911_package_events_i_package_container_push_package.yml
+++ b/config/metrics/counts_all/20210216182911_package_events_i_package_container_push_package.yml
@@ -7,7 +7,8 @@ product_stage: package
product_group: group::package
product_category: container registry
value_type: number
-status: deprecated
+status: removed
+milestone_removed: "14.4"
time_frame: all
data_source: redis
distribution:
diff --git a/config/metrics/counts_all/20210216182917_package_events_i_package_debian_push_package.yml b/config/metrics/counts_all/20210216182917_package_events_i_package_debian_push_package.yml
index 4fe173115c..c6f23fe0c8 100644
--- a/config/metrics/counts_all/20210216182917_package_events_i_package_debian_push_package.yml
+++ b/config/metrics/counts_all/20210216182917_package_events_i_package_debian_push_package.yml
@@ -7,7 +7,8 @@ product_stage: package
product_group: group::package
product_category: package registry
value_type: number
-status: deprecated
+status: removed
+milestone_removed: "14.4"
time_frame: all
data_source: redis
distribution:
diff --git a/config/metrics/counts_all/20210715094459_releases_with_milestones.yml b/config/metrics/counts_all/20210715094459_releases_with_milestones.yml
new file mode 100644
index 0000000000..5d85360458
--- /dev/null
+++ b/config/metrics/counts_all/20210715094459_releases_with_milestones.yml
@@ -0,0 +1,23 @@
+---
+key_path: usage_activity_by_stage.release.releases_with_milestones
+description: Unique users creating releases with milestones associated
+performance_indicator_type: []
+product_section: ops
+product_stage: release
+product_group: 'group::release'
+product_category: Release Orchestration
+value_type: number
+status: active
+milestone: "14.4"
+introduced_by_url: 'https://gitlab.com/gitlab-org/gitlab/-/merge_requests/71287'
+time_frame: 28d
+data_source: database
+instrumentation_class: 'CountUsersAssociatingMilestonesToReleasesMetric'
+data_category: Optional
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
diff --git a/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml b/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml
new file mode 100644
index 0000000000..1a9fe349a9
--- /dev/null
+++ b/config/metrics/counts_all/20210915082040_projects_with_expiration_policy_enabled_with_older_than_set_to_60d.yml
@@ -0,0 +1,22 @@
+---
+data_category: optional
+key_path: counts.projects_with_expiration_policy_enabled_with_older_than_set_to_60d
+description: A count of projects with the cleanup policy set delete tags older than
+ 60 days
+product_section: ops
+product_stage: package
+product_group: group::package
+product_category: container registry
+value_type: number
+status: active
+time_frame: all
+data_source: database
+distribution:
+- ee
+- ce
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/metrics/counts_all/20210916200930_clusters_integrations_prometheus.yml b/config/metrics/counts_all/20210916200930_clusters_integrations_prometheus.yml
new file mode 100644
index 0000000000..2e17c5d261
--- /dev/null
+++ b/config/metrics/counts_all/20210916200930_clusters_integrations_prometheus.yml
@@ -0,0 +1,21 @@
+---
+data_category: optional
+key_path: counts.clusters_integrations_prometheus
+description: Total clusters with Clusters::Integrations::Prometheus enabled
+product_section: ops
+product_stage: configure
+product_group: group::configure
+product_category: kubernetes_management
+value_type: number
+status: active
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml b/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml
new file mode 100644
index 0000000000..54437cfbf3
--- /dev/null
+++ b/config/metrics/counts_all/20210916200931_clusters_integrations_elastic_stack.yml
@@ -0,0 +1,21 @@
+---
+data_category: optional
+key_path: counts.clusters_integrations_elastic_stack
+description: Total clusters with Clusters::Integrations::ElasticStack enabled
+product_section: ops
+product_stage: configure
+product_group: group::configure
+product_category: kubernetes_management
+value_type: number
+status: active
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/metrics/counts_all/20210916202342_clusters_integrations_prometheus.yml b/config/metrics/counts_all/20210916202342_clusters_integrations_prometheus.yml
new file mode 100644
index 0000000000..d0baa4d075
--- /dev/null
+++ b/config/metrics/counts_all/20210916202342_clusters_integrations_prometheus.yml
@@ -0,0 +1,21 @@
+---
+data_category: optional
+key_path: usage_activity_by_stage.monitor.clusters_integrations_prometheus
+description: Users creating clusters with Prometheus integration enabled in last 28 days.
+product_section: ops
+product_stage: monitor
+product_group: group::monitor
+product_category: metrics
+value_type: number
+status: active
+time_frame: all
+data_source: database
+distribution:
+- ce
+- ee
+tier:
+- free
+- premium
+- ultimate
+performance_indicator_type: []
+milestone: "14.4"
diff --git a/config/metrics/license/20210204124829_active_user_count.yml b/config/metrics/license/20210204124829_active_user_count.yml
index 871a75bd97..c6e0895a91 100644
--- a/config/metrics/license/20210204124829_active_user_count.yml
+++ b/config/metrics/license/20210204124829_active_user_count.yml
@@ -10,6 +10,7 @@ status: active
milestone: "<13.9"
data_category: subscription
time_frame: none
+instrumentation_class: ActiveUserCountMetric
data_source: database
distribution:
- ce
diff --git a/config/redis.cache.yml.example b/config/redis.cache.yml.example
deleted file mode 100644
index 44d9f7e863..0000000000
--- a/config/redis.cache.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/10
- #
- # url: redis://localhost:6380
- # sentinels:
- # -
- # host: localhost
- # port: 26380 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26380 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/10
- #
- # url: redis://localhost:6380
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.cache.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6380
- # sentinels:
- # -
- # host: replica1
- # port: 26380 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26380 # point to sentinel, not to redis port
diff --git a/config/redis.queues.yml.example b/config/redis.queues.yml.example
deleted file mode 100644
index 4194b44cb8..0000000000
--- a/config/redis.queues.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/11
- #
- # url: redis://localhost:6381
- # sentinels:
- # -
- # host: localhost
- # port: 26381 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26381 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/11
- #
- # url: redis://localhost:6381
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.queues.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6381
- # sentinels:
- # -
- # host: replica1
- # port: 26381 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26381 # point to sentinel, not to redis port
diff --git a/config/redis.shared_state.yml.example b/config/redis.shared_state.yml.example
deleted file mode 100644
index b3e0c7a8fa..0000000000
--- a/config/redis.shared_state.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/12
- #
- # url: redis://localhost:6382
- # sentinels:
- # -
- # host: localhost
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/12
- #
- # url: redis://localhost:6382
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.shared_state.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6382
- # sentinels:
- # -
- # host: replica1
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
diff --git a/config/redis.trace_chunks.yml.example b/config/redis.trace_chunks.yml.example
deleted file mode 100644
index d38b9ba496..0000000000
--- a/config/redis.trace_chunks.yml.example
+++ /dev/null
@@ -1,38 +0,0 @@
-# If you change this file in a merge request, please also create
-# a merge request on https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests
-#
-development:
- url: redis://localhost:6379/13
- #
- # url: redis://localhost:6382
- # sentinels:
- # -
- # host: localhost
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
-test:
- url: redis://localhost:6379/13
- #
- # url: redis://localhost:6382
-production:
- # Redis (single instance)
- url: unix:/var/run/redis/redis.trace_chunks.sock
- ##
- # Redis + Sentinel (for HA)
- #
- # Please read instructions carefully before using it as you may lose data:
- # http://redis.io/topics/sentinel
- #
- # You must specify a list of a few sentinels that will handle client connection
- # please read here for more information: https://docs.gitlab.com/ee/administration/redis/index.html
- ##
- # url: redis://master:6382
- # sentinels:
- # -
- # host: replica1
- # port: 26382 # point to sentinel, not to redis port
- # -
- # host: replica2
- # port: 26382 # point to sentinel, not to redis port
diff --git a/config/routes.rb b/config/routes.rb
index 8f4c3886e8..01e57a0135 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -161,8 +161,6 @@ Rails.application.routes.draw do
end
end
- resource :projects
-
draw :operations
draw :jira_connect
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index e3b365ad27..dac1937b76 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -38,14 +38,6 @@ namespace :admin do
resources :abuse_reports, only: [:index, :destroy]
resources :gitaly_servers, only: [:index]
- namespace :serverless do
- resources :domains, only: [:index, :create, :update, :destroy] do
- member do
- post '/verify', to: 'domains#verify'
- end
- end
- end
-
resources :spam_logs, only: [:index, :destroy] do
member do
post :mark_as_ham
@@ -69,6 +61,13 @@ namespace :admin do
end
end
+ resources :topics, only: [:index, :new, :create, :edit, :update] do
+ resource :avatar, controller: 'topics/avatars', only: [:destroy]
+ collection do
+ post :preview_markdown
+ end
+ end
+
resources :deploy_keys, only: [:index, :new, :create, :edit, :update, :destroy]
resources :hooks, only: [:index, :create, :edit, :update, :destroy] do
diff --git a/config/routes/group.rb b/config/routes/group.rb
index ef31b639d3..803249f886 100644
--- a/config/routes/group.rb
+++ b/config/routes/group.rb
@@ -146,5 +146,7 @@ scope format: false do
constraints image: Gitlab::PathRegex.container_image_regex, sha: Gitlab::PathRegex.container_image_blob_sha_regex do
get 'v2/*group_id/dependency_proxy/containers/*image/manifests/*tag' => 'groups/dependency_proxy_for_containers#manifest' # rubocop:todo Cop/PutGroupRoutesUnderScope
get 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha' => 'groups/dependency_proxy_for_containers#blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
+ post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload/authorize' => 'groups/dependency_proxy_for_containers#authorize_upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
+ post 'v2/*group_id/dependency_proxy/containers/*image/blobs/:sha/upload' => 'groups/dependency_proxy_for_containers#upload_blob' # rubocop:todo Cop/PutGroupRoutesUnderScope
end
end
diff --git a/config/routes/import.rb b/config/routes/import.rb
index 64830ef1e5..9c76c4435f 100644
--- a/config/routes/import.rb
+++ b/config/routes/import.rb
@@ -12,6 +12,10 @@ end
namespace :import do
resources :available_namespaces, only: [:index], controller: :available_namespaces
+ namespace :url do
+ post :validate
+ end
+
resource :github, only: [:create, :new], controller: :github do
post :personal_access_token
get :status
@@ -66,6 +70,7 @@ namespace :import do
post :configure
get :status
get :realtime_changes
+ get :history
end
resource :manifest, only: [:create, :new], controller: :manifest do
diff --git a/config/routes/project.rb b/config/routes/project.rb
index cbd2f5ac83..b1be9ad2ad 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -298,6 +298,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
end
end
+ resources :cluster_agents, only: [:show], param: :name
+
concerns :clusterable
namespace :serverless do
@@ -311,6 +313,8 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :terraform, only: [:index]
+ resources :google_cloud, only: [:index]
+
resources :environments, except: [:destroy] do
member do
post :stop
diff --git a/config/routes/uploads.rb b/config/routes/uploads.rb
index 71a868175a..e2cdf8ba60 100644
--- a/config/routes/uploads.rb
+++ b/config/routes/uploads.rb
@@ -1,10 +1,10 @@
# frozen_string_literal: true
scope path: :uploads do
- # Note attachments and User/Group/Project avatars
+ # Note attachments and User/Group/Project/Topic avatars
get "-/system/:model/:mounted_as/:id/:filename",
to: "uploads#show",
- constraints: { model: /note|user|group|project/, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
+ constraints: { model: %r{note|user|group|project|projects\/topic}, mounted_as: /avatar|attachment/, filename: %r{[^/]+} }
# show uploads for models, snippets (notes) available for now
get '-/system/:model/:id/:secret/:filename',
diff --git a/config/sidekiq_queues.yml b/config/sidekiq_queues.yml
index d7c6e6031a..56183d167b 100644
--- a/config/sidekiq_queues.yml
+++ b/config/sidekiq_queues.yml
@@ -29,6 +29,8 @@
- 1
- - analytics_code_review_metrics
- 1
+- - analytics_cycle_analytics_group_data_loader
+ - 1
- - analytics_devops_adoption_create_snapshot
- 1
- - analytics_usage_trends_counter_job
@@ -91,6 +93,10 @@
- 1
- - dependency_proxy
- 1
+- - dependency_proxy_blob
+ - 1
+- - dependency_proxy_manifest
+ - 1
- - deployment
- 3
- - design_management_copy_design_collection
@@ -273,8 +279,6 @@
- 1
- - pages_domain_verification
- 1
-- - pages_remove
- - 1
- - pages_transfer
- 1
- - pages_update_configuration
diff --git a/config/webpack.config.js b/config/webpack.config.js
index adb11548a8..e1a48ee2b4 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -342,6 +342,14 @@ module.exports = {
esModule: false,
},
},
+ {
+ test: /editor\/schema\/.+\.json$/,
+ type: 'javascript/auto',
+ loader: 'file-loader',
+ options: {
+ name: '[name].[contenthash:8].[ext]',
+ },
+ },
],
},
diff --git a/danger/database/Dangerfile b/danger/database/Dangerfile
index 3018196ddb..693c03b9da 100644
--- a/danger/database/Dangerfile
+++ b/danger/database/Dangerfile
@@ -33,7 +33,7 @@ MSG
DATABASE_APPROVED_LABEL = 'database::approved'
non_geo_db_schema_updated = !git.modified_files.grep(%r{\Adb/structure\.sql}).empty?
-geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/schema\.rb}).empty?
+geo_db_schema_updated = !git.modified_files.grep(%r{\Aee/db/geo/structure\.sql}).empty?
non_geo_migration_created = !git.added_files.grep(%r{\A(db/(post_)?migrate)/}).empty?
geo_migration_created = !git.added_files.grep(%r{\Aee/db/geo/(post_)?migrate/}).empty?
@@ -45,7 +45,7 @@ if non_geo_migration_created && !non_geo_db_schema_updated
end
if geo_migration_created && !geo_db_schema_updated
- warn format(format_str, migrations: 'Geo migrations', schema: helper.html_link("ee/db/geo/schema.rb"))
+ warn format(format_str, migrations: 'Geo migrations', schema: helper.html_link("ee/db/geo/structure.sql"))
end
return unless helper.ci?
diff --git a/danger/pajamas/Dangerfile b/danger/pajamas/Dangerfile
index a3ff1126bd..fde12c08b3 100644
--- a/danger/pajamas/Dangerfile
+++ b/danger/pajamas/Dangerfile
@@ -59,7 +59,7 @@ MARKDOWN
if blocking_components_in_mr.any?
markdown(<<~MARKDOWN)
- These deprecated components have already been migrated and can no longer be used. Please use [Pajamas components](https://design.gitlab.com/components/status/) instead.
+ These deprecated components have already been migrated and can no longer be used. Please use [Pajamas components](https://design.gitlab.com/components/overview) instead.
* #{blocking_components_in_mr.join("\n* ")}
@@ -70,7 +70,7 @@ end
if deprecated_components_in_mr.any?
markdown(<<~MARKDOWN)
- These deprecated components are in the process of being migrated. Please consider using [Pajamas components](https://design.gitlab.com/components/status/) instead.
+ These deprecated components are in the process of being migrated. Please consider using [Pajamas components](https://design.gitlab.com/components/overview) instead.
* #{deprecated_components_in_mr.join("\n* ")}
diff --git a/danger/plugins/specs.rb b/danger/plugins/specs.rb
new file mode 100644
index 0000000000..3188785487
--- /dev/null
+++ b/danger/plugins/specs.rb
@@ -0,0 +1,10 @@
+# frozen_string_literal: true
+
+require_relative '../../tooling/danger/specs'
+
+module Danger
+ class Specs < ::Danger::Plugin
+ # Put the helper code somewhere it can be tested
+ include Tooling::Danger::Specs
+ end
+end
diff --git a/danger/product_intelligence/Dangerfile b/danger/product_intelligence/Dangerfile
index ae58cf4588..fd6ae76b4f 100644
--- a/danger/product_intelligence/Dangerfile
+++ b/danger/product_intelligence/Dangerfile
@@ -5,20 +5,20 @@ CHANGED_FILES_MESSAGE = <<~MSG
For the following files, a review from the [Data team and Product Intelligence team](https://gitlab.com/groups/gitlab-org/growth/product-intelligence/engineers/-/group_members?with_inherited_permissions=exclude) is recommended
Please check the ~"product intelligence" [guide](https://docs.gitlab.com/ee/development/usage_ping.html).
+For MR review guidelines, see the [Service Ping review guidelines](https://docs.gitlab.com/ee/development/usage_ping/review_guidelines.html) or the [Snowplow review guidelines](https://docs.gitlab.com/ee/development/snowplow/review_guidelines.html).
%s
MSG
-# exit if not matching files
+# exit if not matching files or if no product intelligence labels
matching_changed_files = product_intelligence.matching_changed_files
-return unless matching_changed_files.any?
+labels = product_intelligence.missing_labels
+
+return if matching_changed_files.empty? || labels.empty?
warn format(CHANGED_FILES_MESSAGE, changed_files: helper.markdown_list(matching_changed_files))
-labels = product_intelligence.missing_labels
-return unless labels.any?
-
gitlab.api.update_merge_request(gitlab.mr_json['project_id'],
gitlab.mr_json['iid'],
add_labels: labels)
diff --git a/danger/roulette/Dangerfile b/danger/roulette/Dangerfile
index cd23028a37..54b4680724 100644
--- a/danger/roulette/Dangerfile
+++ b/danger/roulette/Dangerfile
@@ -99,6 +99,9 @@ categories << :database if helper.mr_labels.include?('database')
# Ensure to spin for Product Intelligence reviewer when ~"product intelligence::review pending" is applied
categories << :product_intelligence if helper.mr_labels.include?("product intelligence::review pending")
+# Skip Product intelligence reviews for growth experiment MRs
+categories.delete(:product_intelligence) unless helper.mr_labels.include?("growth experiment")
+
if changes.any?
project = project_helper.project_name
diff --git a/danger/specs/Dangerfile b/danger/specs/Dangerfile
index 35476ae645..117eaf6106 100644
--- a/danger/specs/Dangerfile
+++ b/danger/specs/Dangerfile
@@ -20,7 +20,7 @@ Please make sure the spec files pass in AS-IF-FOSS mode either:
1. Locally with `FOSS_ONLY=1 bin/rspec -- %s`.
1. In the MR pipeline by verifying that the `rspec foss-impact` job has passed.
-1. In the MR pipelines by including `RUN AS-IF-FOSS` in the MR title (you can do it with the ``/title %s [RUN AS-IF-FOSS]`` quick action) and start a new MR pipeline.
+1. In the MR pipelines by setting the ~"pipeline:run-as-if-foss" label on the MR (you can do it with the `/label ~"pipeline:run-as-if-foss"` quick action) and start a new MR pipeline.
MSG
@@ -32,11 +32,12 @@ request specs (and/or feature specs). Please add request specs under
See https://gitlab.com/groups/gitlab-org/-/epics/5076 for information.
MSG
-has_app_changes = helper.all_changed_files.grep(%r{\A(app|lib|db/(geo/)?(post_)?migrate)/}).any?
-has_ee_app_changes = helper.all_changed_files.grep(%r{\Aee/(app|lib|db/(geo/)?(post_)?migrate)/}).any?
-spec_changes = helper.all_changed_files.grep(%r{\Aspec/})
+all_changed_files = helper.all_changed_files
+has_app_changes = all_changed_files.grep(%r{\A(app|lib|db/(geo/)?(post_)?migrate)/}).any?
+has_ee_app_changes = all_changed_files.grep(%r{\Aee/(app|lib|db/(geo/)?(post_)?migrate)/}).any?
+spec_changes = specs.changed_specs_files(ee: :exclude)
has_spec_changes = spec_changes.any?
-has_ee_spec_changes = helper.all_changed_files.grep(%r{\Aee/spec/}).any?
+has_ee_spec_changes = specs.changed_specs_files(ee: :only).any?
new_specs_needed = (gitlab.mr_labels & NO_SPECS_LABELS).empty?
if (has_app_changes || has_ee_app_changes) && !(has_spec_changes || has_ee_spec_changes) && new_specs_needed
@@ -45,10 +46,14 @@ end
# The only changes outside `ee/` are in `spec/`
if has_ee_app_changes && has_spec_changes && !(has_app_changes || has_ee_spec_changes)
- warn format(EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE, spec_files: spec_changes.join(" "), mr_title: gitlab.mr_json['title']), sticky: false
+ warn format(EE_CHANGE_WITH_FOSS_SPEC_CHANGE_MESSAGE, spec_files: spec_changes.join(" ")), sticky: false
end
# Forbidding a new file addition under `/spec/controllers` or `/ee/spec/controllers`
-if git.added_files.grep(%r{^(ee/)?spec/controllers/}).any?
+if project_helper.changes.added.files.grep(%r{^(ee/)?spec/controllers/}).any?
warn CONTROLLER_SPEC_DEPRECATION_MESSAGE
end
+
+specs.changed_specs_files.each do |filename|
+ specs.add_suggestions_for_match_with_array(filename)
+end
diff --git a/data/deprecations/14-0-nfs-fot-git-repository-storage.yml b/data/deprecations/14-0-nfs-fot-git-repository-storage.yml
new file mode 100644
index 0000000000..f38742439a
--- /dev/null
+++ b/data/deprecations/14-0-nfs-fot-git-repository-storage.yml
@@ -0,0 +1,22 @@
+- name: "NFS for Git repository storage deprecated" # The name of the feature to be deprecated
+ announcement_milestone: "14.0" # The milestone when this feature was first announced as deprecated.
+ announcement_date: "2021-06-22" # The date of the milestone release when this feature was first announced as deprecated
+ removal_milestone: "15.2" # The milestone when this feature is planned to be removed
+ body: | # Do not modify this line, instead modify the lines below.
+ With the general availability of Gitaly Cluster ([introduced in GitLab 13.0](https://about.gitlab.com/releases/2020/05/22/gitlab-13-0-released/)), we have deprecated development (bugfixes, performance improvements, etc) for NFS for Git repository storage in GitLab 14.0. We will continue to provide technical support for NFS for Git repositories throughout 14.x, but we will remove all support for NFS in GitLab 15.0. Please see our official [Statement of Support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs) for further information.
+
+ Gitaly Cluster offers tremendous benefits for our customers such as:
+
+ - [Variable replication factors](https://docs.gitlab.com/ee/administration/gitaly/index.html#replication-factor).
+ - [Strong consistency](https://docs.gitlab.com/ee/administration/gitaly/index.html#strong-consistency).
+ - [Distributed read capabilities](https://docs.gitlab.com/ee/administration/gitaly/index.html#distributed-reads).
+
+ We encourage customers currently using NFS for Git repositories to plan their migration by reviewing our documentation on [migrating to Gitaly Cluster](https://docs.gitlab.com/ee/administration/gitaly/index.html#migrate-to-gitaly-cluster).
+
+ stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ issue_url: # (optional) This is a link to the deprecation issue in GitLab
+ documentation_url: # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
+ removal_date: "2022-06-22" # (optional - may be required in the future) YYYY-MM-DD format - the date of the milestone release when this feature is planned to be removed
diff --git a/data/deprecations/14-2-deprecation-task-runner.yml b/data/deprecations/14-2-deprecation-task-runner.yml
new file mode 100644
index 0000000000..4d01a1969a
--- /dev/null
+++ b/data/deprecations/14-2-deprecation-task-runner.yml
@@ -0,0 +1,16 @@
+- name: "Rename Task Runner pod to Toolbox" # The name of the feature to be deprecated
+ announcement_milestone: "14.2" # The milestone when this feature was first announced as deprecated.
+ announcement_date: "2021-08-22" # The date of the milestone release when this feature was first announced as deprecated
+ removal_milestone: "14.4" # The milestone when this feature is planned to be removed
+ body: | # Do not modify this line, instead modify the lines below.
+ The Task Runner pod is used to execute periodic housekeeping tasks within the GitLab application and is often confused with the GitLab Runner. Thus, [Task Runner will be renamed to Toolbox](https://gitlab.com/groups/gitlab-org/charts/-/epics/25).
+
+ This will result in the rename of the sub-chart: `gitlab/task-runner` to `gitlab/toolbox`. Resulting pods will be named along the lines of `{{ .Release.Name }}-toolbox`, which will often be `gitlab-toolbox`. They will be locatable with the label `app=toolbox`.
+
+ stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ issue_url: # (optional) This is a link to the deprecation issue in GitLab
+ documentation_url: # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
+ removal_date: "2021-10-22" # (optional - may be required in the future) YYYY-MM-DD format - the date of the milestone release when this feature is planned to be removed
diff --git a/data/deprecations/14-3-deprecation-release-cli.yml b/data/deprecations/14-3-deprecation-release-cli.yml
new file mode 100644
index 0000000000..4273d00fc7
--- /dev/null
+++ b/data/deprecations/14-3-deprecation-release-cli.yml
@@ -0,0 +1,13 @@
+- name: "Release CLI be distributed as a generic package" # The name of the feature to be deprecated
+ announcement_milestone: "14.2" # The milestone when this feature was first announced as deprecated.
+ announcement_date: "2021-08-22" # The date of the milestone release when this feature was first announced as deprecated
+ removal_milestone: "14.6" # The milestone when this feature is planned to be removed
+ body: | # Do not modify this line, instead modify the lines below.
+ The [release-cli](https://gitlab.com/gitlab-org/release-cli) will be released as a [generic package](https://gitlab.com/gitlab-org/release-cli/-/packages) starting in GitLab 14.2. We will continue to deploy it as a binary to S3 until GitLab 14.5 and stop distributing it in S3 in GitLab 14.6.
+ stage: # (optional - may be required in the future) String value of the stage that the feature was created in. e.g., Growth
+ tiers: # (optional - may be required in the future) An array of tiers that the feature is available in currently. e.g., [Free, Silver, Gold, Core, Premium, Ultimate]
+ issue_url: # (optional) This is a link to the deprecation issue in GitLab
+ documentation_url: # (optional) This is a link to the current documentation page
+ image_url: # (optional) This is a link to a thumbnail image depicting the feature
+ video_url: # (optional) Use the youtube thumbnail URL with the structure of https://img.youtube.com/vi/UNIQUEID/hqdefault.jpg
+ removal_date: "2021-12-22" # (optional - may be required in the future) YYYY-MM-DD format - the date of the milestone release when this feature is planned to be removed
diff --git a/data/deprecations/14-3-repository-push-audit-events.yml b/data/deprecations/14-3-repository-push-audit-events.yml
index 3a39c1f430..4bca8751db 100644
--- a/data/deprecations/14-3-repository-push-audit-events.yml
+++ b/data/deprecations/14-3-repository-push-audit-events.yml
@@ -11,4 +11,4 @@
tiers: Premium
issue_url: https://gitlab.com/gitlab-org/gitlab/-/issues/337993
documentation_url: https://docs.gitlab.com/ee/administration/audit_events.html#repository-push
- announcement_date: "2021-09-02" # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69024
+ announcement_date: "2021-09-22" # https://gitlab.com/gitlab-org/gitlab/-/merge_requests/69024
diff --git a/data/deprecations/distribution_deprecations_14-3.yml b/data/deprecations/distribution_deprecations_14-3.yml
deleted file mode 100644
index 0526338315..0000000000
--- a/data/deprecations/distribution_deprecations_14-3.yml
+++ /dev/null
@@ -1,8 +0,0 @@
-- name: "Rename Task Runner pod to Toolbox" # The name of the feature to be deprecated
- announcement_milestone: "14.2" # The milestone when this feature was first announced as deprecated.
- announcement_date: "2021-09-22"
- removal_milestone: "14.4" # the milestone when this feature is planned to be removed
- body: | # Do not modify this line, instead modify the lines below.
- The Task Runner pod is used to execute periodic housekeeping tasks within the GitLab application and is often confused with the GitLab Runner. Thus, [Task Runner will be renamed to Toolbox](https://gitlab.com/groups/gitlab-org/charts/-/epics/25).
-
- This will result in the rename of the sub-chart: `gitlab/task-runner` to `gitlab/toolbox`. Resulting pods will be named along the lines of `{{ .Release.Name }}-toolbox`, which will often be `gitlab-toolbox`. They will be locatable with the label `app=toolbox`.
diff --git a/data/deprecations/templates/_deprecation_template.md.erb b/data/deprecations/templates/_deprecation_template.md.erb
index 64bd6a75a5..a037151c6a 100644
--- a/data/deprecations/templates/_deprecation_template.md.erb
+++ b/data/deprecations/templates/_deprecation_template.md.erb
@@ -6,6 +6,16 @@ info: "See the Technical Writers assigned to Development Guidelines: https://abo
# Deprecated feature removal schedule
+DISCLAIMER:
+This page contains information related to upcoming products, features, and functionality.
+It is important to note that the information presented is for informational purposes only.
+Please do not rely on this information for purchasing or planning purposes.
+As with all projects, the items mentioned on this page are subject to change or delay.
+The development, release, and timing of any products, features, or functionality remain at the
+sole discretion of GitLab Inc.
+
+
+
-
diff --git a/doc/administration/audit_events.md b/doc/administration/audit_events.md
index 3ff5fb2635..572c341f2b 100644
--- a/doc/administration/audit_events.md
+++ b/doc/administration/audit_events.md
@@ -137,8 +137,6 @@ Project event queries are limited to a maximum of 30 days.
### Instance events **(PREMIUM SELF)**
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/2336) in GitLab 9.3.
-
Server-wide audit events introduce the ability to observe user actions across
the entire instance of your GitLab server, making it easy to understand who
changed what and when for audit purposes.
diff --git a/doc/administration/auth/README.md b/doc/administration/auth/README.md
deleted file mode 100644
index 5ab8653dc3..0000000000
--- a/doc/administration/auth/README.md
+++ /dev/null
@@ -1,8 +0,0 @@
----
-redirect_to: 'index.md'
----
-
-This document was moved to [another location](index.md).
-
-
-
diff --git a/doc/administration/auth/index.md b/doc/administration/auth/index.md
index a072cc73c4..959c521855 100644
--- a/doc/administration/auth/index.md
+++ b/doc/administration/auth/index.md
@@ -31,7 +31,7 @@ providers:
- [Salesforce](../../integration/salesforce.md)
- [SAML](../../integration/saml.md)
- [SAML for GitLab.com groups](../../user/group/saml_sso/index.md) **(PREMIUM SAAS)**
-- [Shibboleth](../../integration/shibboleth.md)
+- [Shibboleth](../../integration/saml.md)
- [Smartcard](smartcard.md) **(PREMIUM SELF)**
- [Twitter](../../integration/twitter.md)
@@ -45,7 +45,7 @@ For more information, see the links shown on this page for each external provide
| Capability | SaaS | Self-Managed |
|-------------------------------------------------|-----------------------------------------|------------------------------------|
-| **User Provisioning** | SCIM
JIT Provisioning | LDAP Sync |
+| **User Provisioning** | SCIM
Just-In-Time (JIT) Provisioning | LDAP Sync |
| **User Detail Updating** (not group management) | Not Available | LDAP Sync |
| **Authentication** | SAML at top-level group (1 provider) | LDAP (multiple providers)
Generic OAuth2
SAML (only 1 permitted per unique provider)
Kerberos
JWT
Smartcard
OmniAuth Providers (only 1 permitted per unique provider) |
| **Provider-to-GitLab Role Sync** | SAML Group Sync | LDAP Group Sync |
diff --git a/doc/administration/auth/ldap/google_secure_ldap.md b/doc/administration/auth/ldap/google_secure_ldap.md
index 137f35986a..e5af8e8256 100644
--- a/doc/administration/auth/ldap/google_secure_ldap.md
+++ b/doc/administration/auth/ldap/google_secure_ldap.md
@@ -15,7 +15,7 @@ LDAP service that can be configured with GitLab for authentication and group syn
Secure LDAP requires a slightly different configuration than standard LDAP servers.
The steps below cover:
-- Configuring the Secure LDAP Client in the Google Admin console.
+- Configuring the Secure LDAP Client in the Google administrator console.
- Required GitLab configuration.
## Configuring Google LDAP client
diff --git a/doc/administration/auth/ldap/index.md b/doc/administration/auth/ldap/index.md
index 1992b45033..92815f10b9 100644
--- a/doc/administration/auth/ldap/index.md
+++ b/doc/administration/auth/ldap/index.md
@@ -5,7 +5,7 @@ group: Access
info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
---
-# General LDAP setup **(FREE SELF)**
+# Integrate LDAP with GitLab **(FREE SELF)**
GitLab integrates with [LDAP](https://en.wikipedia.org/wiki/Lightweight_Directory_Access_Protocol)
to support user authentication.
@@ -39,7 +39,9 @@ the LDAP server, or share email addresses.
### User deletion
Users deleted from the LDAP server are immediately blocked from signing in
-to GitLab. However, there's an LDAP check cache time of one hour (which is
+to GitLab and [no longer consumes a
+license](../../../user/admin_area/moderate_users.md).
+However, there's an LDAP check cache time of one hour (which is
[configurable](#adjust-ldap-user-sync-schedule) for GitLab Premium users).
This means users already signed-in or who are using Git over SSH can access
GitLab for up to one hour. Manually block the user in the GitLab Admin Area
@@ -70,15 +72,15 @@ LDAP email address, and then sign into GitLab by using their LDAP credentials.
LDAP service that can be configured with GitLab for authentication and group sync.
See [Google Secure LDAP](google_secure_ldap.md) for detailed configuration instructions.
-## Configuration
+## Configure LDAP
-To enable LDAP integration you must add your LDAP server settings in
-`/etc/gitlab/gitlab.rb` or `/home/git/gitlab/config/gitlab.yml` for Omnibus
-GitLab and installations from source respectively.
+To configure LDAP integration, add your LDAP server settings in:
-There is a Rake task to check LDAP configuration. After configuring LDAP
-using the documentation below, see [LDAP check Rake task](../../raketasks/check.md#ldap-check)
-for information on the LDAP check Rake task.
+- `/etc/gitlab/gitlab.rb` for Omnibus GitLab instances.
+- `/home/git/gitlab/config/gitlab.yml` for source install instances.
+
+After configuring LDAP, to test the configuration, use the
+[LDAP check Rake task](../../raketasks/check.md#ldap-check).
NOTE:
The `encryption` value `simple_tls` corresponds to 'Simple TLS' in the LDAP
@@ -90,9 +92,9 @@ with `start_tls` and `ssl` was replaced with `simple_tls`.
LDAP users must have a set email address, regardless of whether or not it's used
to sign in.
-### Example Configurations
+### Example Omnibus GitLab configuration
-**Omnibus Configuration**
+This example shows configuration for Omnibus GitLab instances:
```ruby
gitlab_rails['ldap_enabled'] = true
@@ -139,7 +141,9 @@ gitlab_rails['ldap_servers'] = {
}
```
-**Source Configuration**
+### Example source install configuration
+
+This example shows configuration for source install instances:
```yaml
production:
@@ -155,6 +159,8 @@ production:
### Basic configuration settings
+These configuration settings are available:
+
| Setting | Description | Required | Examples |
|--------------------|-------------|----------|----------|
| `label` | A human-friendly name for your LDAP server. It is displayed on your sign-in page. | **{check-circle}** Yes | `'Paris'` or `'Acme, Ltd.'` |
@@ -183,6 +189,8 @@ Some examples of the `user_filter` field syntax:
### SSL configuration settings
+These SSL configuration settings are available:
+
| Setting | Description | Required | Examples |
|---------------|-------------|----------|----------|
| `ca_file` | Specifies the path to a file containing a PEM-format CA certificate, for example, if you need an internal CA. | **{dotted-circle}** No | `'/etc/ca.pem'` |
@@ -193,69 +201,72 @@ Some examples of the `user_filter` field syntax:
### Attribute configuration settings
-LDAP attributes that GitLab uses to create an account for the LDAP user. The specified
-attribute can either be the attribute name as a string (for example, `'mail'`), or an
-array of attribute names to try in order (for example, `['mail', 'email']`).
-The user's LDAP sign-in is the attribute specified as `uid` above.
+GitLab uses these LDAP attributes to create an account for the LDAP user. The specified
+attribute can be either:
+
+- The attribute name as a string. For example, `'mail'`.
+- An array of attribute names to try in order. For example, `['mail', 'email']`.
+
+The user's LDAP sign in is the LDAP attribute [specified as `uid`](#basic-configuration-settings).
| Setting | Description | Required | Examples |
|--------------|-------------|----------|----------|
-| `username` | The username is used in paths for the user's own projects (like `gitlab.example.com/username/project`) and when mentioning them in issues, merge request and comments (like `@username`). If the attribute specified for `username` contains an email address, the GitLab username is part of the email address before the `@`. | **{dotted-circle}** No | `['uid', 'userid', 'sAMAccountName']` |
+| `username` | Used in paths for the user's own projects (for example, `gitlab.example.com/username/project`) and when mentioning them in issues, merge request and comments (for example, `@username`). If the attribute specified for `username` contains an email address, the GitLab username is part of the email address before the `@`. | **{dotted-circle}** No | `['uid', 'userid', 'sAMAccountName']` |
| `email` | LDAP attribute for user email. | **{dotted-circle}** No | `['mail', 'email', 'userPrincipalName']` |
| `name` | LDAP attribute for user display name. If `name` is blank, the full name is taken from the `first_name` and `last_name`. | **{dotted-circle}** No | Attributes `'cn'`, or `'displayName'` commonly carry full names. Alternatively, you can force the use of `first_name` and `last_name` by specifying an absent attribute such as `'somethingNonExistent'`. |
| `first_name` | LDAP attribute for user first name. Used when the attribute configured for `name` does not exist. | **{dotted-circle}** No | `'givenName'` |
| `last_name` | LDAP attribute for user last name. Used when the attribute configured for `name` does not exist. | **{dotted-circle}** No | `'sn'` |
-### LDAP Sync configuration settings **(PREMIUM SELF)**
+### LDAP sync configuration settings **(PREMIUM SELF)**
+
+These LDAP sync configuration settings are available:
| Setting | Description | Required | Examples |
|-------------------|-------------|----------|----------|
| `group_base` | Base used to search for groups. | **{dotted-circle}** No | `'ou=groups,dc=gitlab,dc=example'` |
-| `admin_group` | The CN of a group containing GitLab administrators. Note: Not `cn=administrators` or the full DN. | **{dotted-circle}** No | `'administrators'` |
-| `external_groups` | An array of CNs of groups containing users that should be considered external. Note: Not `cn=interns` or the full DN. | **{dotted-circle}** No | `['interns', 'contractors']` |
+| `admin_group` | The CN of a group containing GitLab administrators. Not `cn=administrators` or the full DN. | **{dotted-circle}** No | `'administrators'` |
+| `external_groups` | An array of CNs of groups containing users that should be considered external. Not `cn=interns` or the full DN. | **{dotted-circle}** No | `['interns', 'contractors']` |
| `sync_ssh_keys` | The LDAP attribute containing a user's public SSH key. | **{dotted-circle}** No | `'sshPublicKey'` or false if not set |
### Set up LDAP user filter
-If you want to limit all GitLab access to a subset of the LDAP users on your
-LDAP server, the first step should be to narrow the configured `base`. However,
-it's sometimes necessary to further filter users. In this case, you can set
-up an LDAP user filter. The filter must comply with
-[RFC 4515](https://tools.ietf.org/search/rfc4515).
+To limit all GitLab access to a subset of the LDAP users on your LDAP server, first narrow the
+configured `base`. However, to further filter users if
+necessary, you can set up an LDAP user filter. The filter must comply with [RFC 4515](https://tools.ietf.org/search/rfc4515).
-**Omnibus configuration**
+- Example user filter for Omnibus GitLab instances:
-```ruby
-gitlab_rails['ldap_servers'] = {
-'main' => {
- # snip...
- 'user_filter' => '(employeeType=developer)'
+ ```ruby
+ gitlab_rails['ldap_servers'] = {
+ 'main' => {
+ # snip...
+ 'user_filter' => '(employeeType=developer)'
+ }
}
-}
-```
+ ```
-**Source configuration**
+- Example user filter for source install instances:
-```yaml
-production:
- ldap:
- servers:
- main:
- # snip...
- user_filter: '(employeeType=developer)'
-```
+ ```yaml
+ production:
+ ldap:
+ servers:
+ main:
+ # snip...
+ user_filter: '(employeeType=developer)'
+ ```
-If you want to limit access to the nested members of an Active Directory
-group, use the following syntax:
+To limit access to the nested members of an Active Directory group, use the following syntax:
```plaintext
(memberOf:1.2.840.113556.1.4.1941:=CN=My Group,DC=Example,DC=com)
```
-For more information about this "LDAP_MATCHING_RULE_IN_CHAIN" filter, see the following
-[Microsoft Search Filter Syntax](https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax) document.
+For more information about `LDAP_MATCHING_RULE_IN_CHAIN` filters, see
+[Search Filter Syntax](https://docs.microsoft.com/en-us/windows/win32/adsi/search-filter-syntax).
+
Support for nested members in the user filter shouldn't be confused with
-[group sync nested groups support](#supported-ldap-group-typesattributes). **(PREMIUM SELF)**
+[group sync nested groups](#supported-ldap-group-typesattributes) support.
GitLab does not support the custom filter syntax used by OmniAuth LDAP.
@@ -451,7 +462,7 @@ If initially your LDAP configuration looked like:
### TLS server authentication
-There are two encryption methods, `simple_tls` and `start_tls`.
+`simple_tls` and `start_tls` are the two available encryption methods.
For either encryption method, if setting `verify_certificates: false`, TLS
encryption is established with the LDAP server before any LDAP-protocol data is
@@ -463,9 +474,9 @@ exchanged but no validation of the LDAP server's SSL certificate is performed.
Not implemented by `Net::LDAP`.
-You should disable anonymous LDAP authentication and enable simple or SASL
-authentication. The TLS client authentication setting in your LDAP server cannot
-be mandatory and clients cannot be authenticated with the TLS protocol.
+You should disable anonymous LDAP authentication and enable simple or Simple Authentication
+and Security Layer (SASL) authentication. The TLS client authentication setting in your LDAP server
+cannot be mandatory and clients cannot be authenticated with the TLS protocol.
## Multiple LDAP servers **(PREMIUM SELF)**
@@ -474,7 +485,7 @@ connects to.
To add another LDAP server:
-1. Duplicate the settings under [the main configuration](#configuration).
+1. Duplicate the settings under [the main configuration](#configure-ldap).
1. Edit them to match the additional LDAP server.
Be sure to choose a different provider ID made of letters a-z and numbers 0-9.
@@ -526,7 +537,7 @@ The process executes the following access checks:
- Ensure the user is still present in LDAP.
- If the LDAP server is Active Directory, ensure the user is active (not
- blocked/disabled state). This is checked only if
+ blocked/disabled state). This check is performed only if
`active_directory: true` is set in the LDAP configuration.
In Active Directory, a user is marked as disabled/blocked if the user
@@ -702,7 +713,7 @@ When enabled, the following applies:
To enable it, you must:
-1. [Enable LDAP](#configuration)
+1. [Configure LDAP](#configure-ldap).
1. On the top bar, select **Menu > Admin**.
1. On the left sidebar, select **Settings > General**.
1. Expand the **Visibility and access controls** section.
@@ -716,7 +727,7 @@ The values shown are in cron format. If needed, you can use a
WARNING:
Do not start the sync process too frequently as this
-could lead to multiple syncs running concurrently. This is primarily a concern
+could lead to multiple syncs running concurrently. This concern is primarily
for installations with a large number of LDAP users. Review the
[LDAP group sync benchmark metrics](#benchmarks) to see how
your installation compares before proceeding.
@@ -850,7 +861,7 @@ LDAP group links each:
- Subsequent syncs (checking membership, no writes) took 15 minutes
These metrics are meant to provide a baseline and performance may vary based on
-any number of factors. This was an extreme benchmark and most instances don't
+any number of factors. This benchmark was extreme and most instances don't
have near this many users or groups. Disk speed, database performance,
network and LDAP server response time affects these metrics.
diff --git a/doc/administration/auth/ldap/ldap-troubleshooting.md b/doc/administration/auth/ldap/ldap-troubleshooting.md
index 1952e8afa9..4757725d0b 100644
--- a/doc/administration/auth/ldap/ldap-troubleshooting.md
+++ b/doc/administration/auth/ldap/ldap-troubleshooting.md
@@ -55,9 +55,8 @@ main: # 'main' is the GitLab 'provider ID' of this LDAP server
#### Query LDAP **(PREMIUM SELF)**
The following allows you to perform a search in LDAP using the rails console.
-Depending on what you're trying to do, it may make more sense to query [a
-user](#query-a-user-in-ldap) or [a group](#query-a-group-in-ldap) directly, or
-even [use `ldapsearch`](#ldapsearch) instead.
+Depending on what you're trying to do, it may make more sense to query [a user](#query-a-user-in-ldap)
+or [a group](#query-a-group-in-ldap) directly, or even [use `ldapsearch`](#ldapsearch) instead.
```ruby
adapter = Gitlab::Auth::Ldap::Adapter.new('ldapmain')
@@ -90,7 +89,7 @@ established but GitLab doesn't show you LDAP users in the output, one of the
following is most likely true:
- The `bind_dn` user doesn't have enough permissions to traverse the user tree.
-- The user(s) don't fall under the [configured `base`](index.md#configuration).
+- The user(s) don't fall under the [configured `base`](index.md#configure-ldap).
- The [configured `user_filter`](index.md#set-up-ldap-user-filter) blocks access to the user(s).
In this case, you con confirm which of the above is true using
@@ -102,7 +101,7 @@ In this case, you con confirm which of the above is true using
A user can have trouble signing in for any number of reasons. To get started,
here are some questions to ask yourself:
-- Does the user fall under the [configured `base`](index.md#configuration) in
+- Does the user fall under the [configured `base`](index.md#configure-ldap) in
LDAP? The user must fall under this `base` to sign in.
- Does the user pass through the [configured `user_filter`](index.md#set-up-ldap-user-filter)?
If one is not configured, this question can be ignored. If it is, then the
@@ -355,11 +354,10 @@ things to check to debug the situation.
1. Select the **Identities** tab. There should be an LDAP identity with
an LDAP DN as the 'Identifier'. If not, this user hasn't signed in with
LDAP yet and must do so first.
-- You've waited an hour or [the configured
- interval](index.md#adjust-ldap-group-sync-schedule) for the group to
- sync. To speed up the process, either go to the GitLab group **Group information > Members**
- and press **Sync now** (sync one group) or [run the group sync Rake
- task](../../raketasks/ldap.md#run-a-group-sync) (sync all groups).
+- You've waited an hour or [the configured interval](index.md#adjust-ldap-group-sync-schedule) for
+ the group to sync. To speed up the process, either go to the GitLab group **Group information > Members**
+ and press **Sync now** (sync one group) or [run the group sync Rake task](../../raketasks/ldap.md#run-a-group-sync)
+ (sync all groups).
If all of the above looks good, jump in to a little more advanced debugging in
the rails console.
@@ -371,8 +369,8 @@ the rails console.
1. Look through the output of the sync. See [example log
output](#example-console-output-after-a-group-sync)
for how to read the output.
-1. If you still aren't able to see why the user isn't being added, [query the
- LDAP group directly](#query-a-group-in-ldap) to see what members are listed.
+1. If you still aren't able to see why the user isn't being added, [query the LDAP group directly](#query-a-group-in-ldap)
+ to see what members are listed.
1. Is the user's DN or UID in one of the lists from the above output? One of the DNs or
UIDs here should match the 'Identifier' from the LDAP identity checked earlier. If it doesn't,
the user does not appear to be in the LDAP group.
@@ -387,7 +385,7 @@ the following are true:
- The configured `admin_group` in the `gitlab.rb` is a CN, rather than a DN or an array.
- This CN falls under the scope of the configured `group_base`.
- The members of the `admin_group` have already signed into GitLab with their LDAP
- credentials. GitLab only grants this administrator access to the users whose
+ credentials. GitLab only grants the Administrator role to the users whose
accounts are already connected to LDAP.
If all the above are true and the users are still not getting access, [run a manual
@@ -398,8 +396,8 @@ GitLab syncs the `admin_group`.
#### Sync all groups
NOTE:
-To sync all groups manually when debugging is unnecessary, [use the Rake
-task](../../raketasks/ldap.md#run-a-group-sync) instead.
+To sync all groups manually when debugging is unnecessary,
+[use the Rake task](../../raketasks/ldap.md#run-a-group-sync) instead.
The output from a manual [group sync](index.md#group-sync) can show you what happens
when GitLab syncs its LDAP group memberships against LDAP.
diff --git a/doc/administration/auth/oidc.md b/doc/administration/auth/oidc.md
index 6a037e75f5..12729f2050 100644
--- a/doc/administration/auth/oidc.md
+++ b/doc/administration/auth/oidc.md
@@ -228,8 +228,7 @@ Azure B2C [offers two ways of defining the business logic for logging in a user]
While cumbersome to configure, custom policies are required because
standard Azure B2C user flows [do not send the OpenID `email` claim](https://github.com/MicrosoftDocs/azure-docs/issues/16566). In
-other words, they do not work with the [`allow_single_sign_on` or `auto_link_user`
-parameters](../../integration/omniauth.md#initial-omniauth-configuration).
+other words, they do not work with the [`allow_single_sign_on` or `auto_link_user` parameters](../../integration/omniauth.md#initial-omniauth-configuration).
With a standard Azure B2C policy, GitLab cannot create a new account or
link to an existing one with an email address.
diff --git a/doc/administration/auth/smartcard.md b/doc/administration/auth/smartcard.md
index 7e2699d5eb..d79837776b 100644
--- a/doc/administration/auth/smartcard.md
+++ b/doc/administration/auth/smartcard.md
@@ -126,7 +126,7 @@ more information, see [the relevant issue](https://gitlab.com/gitlab-org/gitlab/
gitlab_rails['smartcard_client_certificate_required_port'] = 3444
```
- NOTE: **Note**
+ NOTE:
Assign a value to at least one of the following variables:
`gitlab_rails['smartcard_client_certificate_required_host']` or
`gitlab_rails['smartcard_client_certificate_required_port']`.
diff --git a/doc/administration/clusters/kas.md b/doc/administration/clusters/kas.md
index 6afaff7339..226710a891 100644
--- a/doc/administration/clusters/kas.md
+++ b/doc/administration/clusters/kas.md
@@ -104,7 +104,7 @@ In Omnibus GitLab, find the logs in `/var/log/gitlab/gitlab-kas/`.
See also the [user documentation](../../user/clusters/agent/index.md#troubleshooting)
for troubleshooting problems with individual agents.
-### KAS logs - GitOps: failed to get project info
+### KAS logs - GitOps: failed to get project information
If you get the following error message:
diff --git a/doc/administration/configure.md b/doc/administration/configure.md
index d3e37b4a0e..822acc1a74 100644
--- a/doc/administration/configure.md
+++ b/doc/administration/configure.md
@@ -7,10 +7,44 @@ type: reference
# Configure your GitLab installation **(FREE SELF)**
-Customize and configure your self-managed GitLab installation.
+Customize and configure your self-managed GitLab installation. Here are some quick links to get you started:
- [Authentication](auth/index.md)
- [Configuration](../user/admin_area/index.md)
- [Repository storage](repository_storage_paths.md)
- [Geo](geo/index.md)
- [Packages](packages/index.md)
+
+The following tables are intended to guide you to choose the right combination of capabilties based on your requirements. It is common to want the most
+available, quickly recoverable, highly performant and fully data resilient solution. However, there are tradeoffs.
+
+The tables lists features on the left and provides their capabilities to the right along with known trade-offs.
+
+## Gitaly Capabilities
+
+| | Availability | Recoverability | Data Resiliency | Performance | Risks/Trade-offs|
+|-|--------------|----------------|-----------------|-------------|-----------------|
+|Gitaly Cluster | Very high - tolerant of node failures | RTO for a single node of 10s with no manual intervention | Data is stored on multiple nodes | Good - While writes may take slightly longer due to voting, read distribution improves read speeds | **Trade-off** - Slight decrease in write speed for redundant, strongly-consistent storage solution. **Risks** - [Does not currently support snapshot backups](gitaly/index.md#snapshot-backup-and-recovery-limitations), GitLab backup task can be slow for large data sets |
+|Gitaly Shards | Single storage location is a single point of failure | Would need to restore only shards which failed | Single point of failure | Good - can allocate repositories to shards to spread load | **Trade-off** - Need to manually configure repositories into different shards to balance loads / storage space **Risks** - Single point of failure relies on recovery process when single-node failure occurs |
+|Gitaly + NFS | Single storage location is a single point of failure | Single node failure requires restoration from backup | Single point of failure | Average - NFS is not ideally suited to large quantities of small reads / writes which can have a detrimental impact on performance | **Trade-off** - Easy and familiar administration though NFS is not ideally suited to Git demands **Risks** - Many instances of NFS compatibility issues which provide very poor customer experiences |
+
+## Geo Capabilities
+
+If your availabity needs to span multiple zones or multiple locations, please read about [Geo](geo/index.md).
+
+| | Availability | Recoverability | Data Resiliency | Performance | Risks/Trade-offs|
+|-|--------------|----------------|-----------------|-------------|-----------------|
+|Geo| Depends on the architecture of the Geo site. It is possible to deploy secondaries in single and multiple node configurations. | Eventually consistent. Recovery point depends on replication lag, which depends on a number of factors such as network speeds. Geo supports failover from a primary to secondary site using manual commands that are scriptable. | Geo currently replicates 100% of planned data types and verifies 50%. See [limitations table](geo/replication/datatypes.md#limitations-on-replicationverification) for more detail. | Improves read/clone times for users of a secondary. | Geo is not intended to replace other backup/restore solutions. Because of replication lag and the possibility of replicating bad data from a primary, we recommend that customers also take regular backups of their primary site and test the restore process. |
+
+## Scenarios for failure modes and available mitigation paths
+
+The following table outlines failure modes and mitigation paths for the product offerings detailed in the tables above. Note - Gitaly Cluster install assumes an odd number replication factor of 3 or greater
+
+| Gitaly Mode | Loss of Single Gitaly Node | Application / Data Corruption | Regional Outage (Loss of Instance) | Notes |
+| ----------- | -------------------------- | ----------------------------- | ---------------------------------- | ----- |
+| Single Gitaly Node | Downtime - Must restore from backup | Downtime - Must restore from Backup | Downtime - Must wait for outage to end | |
+| Single Gitaly Node + Geo Secondary | Downtime - Must restore from backup, can perform a manual failover to secondary | Downtime - Must restore from Backup, errors could have propagated to secondary | Manual intervention - failover to Geo secondary | |
+| Sharded Gitaly Install | Partial Downtime - Only repos on impacted node affected, must restore from backup | Partial Downtime - Only repos on impacted node affected, must restore from backup | Downtime - Must wait for outage to end | |
+| Sharded Gitaly Install + Geo Secondary | Partial Downtime - Only repos on impacted node affected, must restore from backup, could perform manual failover to secondary for impacted repos | Partial Downtime - Only repos on impacted node affected, must restore from backup, errors could have propagated to secondary | Manual intervention - failover to Geo secondary | |
+| Gitaly Cluster Install* | No Downtime - will swap repository primary to another node after 10 seconds | N/A - All writes are voted on by multiple Gitaly Cluster nodes | Downtime - Must wait for outage to end | Snapshot backups for Gitaly Cluster nodes not supported at this time |
+| Gitaly Cluster Install* + Geo Secondary | No Downtime - will swap repository primary to another node after 10 seconds | N/A - All writes are voted on by multiple Gitaly Cluster nodes | Manual intervention - failover to Geo secondary | Snapshot backups for Gitaly Cluster nodes not supported at this time |
diff --git a/doc/administration/environment_variables.md b/doc/administration/environment_variables.md
index 057abce0ed..3af8036391 100644
--- a/doc/administration/environment_variables.md
+++ b/doc/administration/environment_variables.md
@@ -21,6 +21,7 @@ You can use the following environment variables to override certain values:
|--------------------------------------------|---------|---------------------------------------------------------------------------------------------------------|
| `DATABASE_URL` | string | The database URL; is of the form: `postgresql://localhost/blog_development`. |
| `ENABLE_BOOTSNAP` | string | Enables Bootsnap for speeding up initial Rails boot (`1` to enable). |
+| `EXTERNAL_URL` | string | Specify the external URL at the [time of installation](https://docs.gitlab.com/omnibus/settings/configuration.html#specifying-the-external-url-at-the-time-of-installation). |
| `EXTERNAL_VALIDATION_SERVICE_TIMEOUT` | integer | Timeout, in seconds, for an [external CI/CD pipeline validation service](external_pipeline_validation.md). Default is `5`. |
| `EXTERNAL_VALIDATION_SERVICE_URL` | string | URL to an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
| `EXTERNAL_VALIDATION_SERVICE_TOKEN` | string | The `X-Gitlab-Token` for authentication with an [external CI/CD pipeline validation service](external_pipeline_validation.md). |
diff --git a/doc/administration/external_pipeline_validation.md b/doc/administration/external_pipeline_validation.md
index 738cf59121..a4ed287cc3 100644
--- a/doc/administration/external_pipeline_validation.md
+++ b/doc/administration/external_pipeline_validation.md
@@ -76,7 +76,8 @@ required number of seconds.
"email": { "type": "string" },
"created_at": { "type": ["string", "null"], "format": "date-time" },
"current_sign_in_ip": { "type": ["string", "null"] },
- "last_sign_in_ip": { "type": ["string", "null"] }
+ "last_sign_in_ip": { "type": ["string", "null"] },
+ "sign_in_count": { "type": "integer" }
}
},
"pipeline": {
diff --git a/doc/administration/feature_flags.md b/doc/administration/feature_flags.md
index 575fa9eb22..f2067e7a2d 100644
--- a/doc/administration/feature_flags.md
+++ b/doc/administration/feature_flags.md
@@ -1,8 +1,7 @@
---
-stage: none
-group: Development
-info: "See the Technical Writers assigned to Development Guidelines: https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments-to-development-guidelines"
-type: reference
+stage: Enablement
+group: Distribution
+info: To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments
description: "GitLab administrator: enable and disable GitLab features deployed behind feature flags"
---
diff --git a/doc/administration/file_hooks.md b/doc/administration/file_hooks.md
index f73c961f54..e041f1e11c 100644
--- a/doc/administration/file_hooks.md
+++ b/doc/administration/file_hooks.md
@@ -7,16 +7,15 @@ type: reference
# File hooks **(FREE SELF)**
-> - Introduced in GitLab 10.6.
-> - Until GitLab 12.8, the feature name was Plugins.
+> Renamed feature from Plugins to File hooks in GitLab 12.8.
With custom file hooks, GitLab administrators can introduce custom integrations
without modifying the GitLab source code.
-NOTE:
-Instead of writing and supporting your own file hook you can make changes
-directly to the GitLab source code and contribute back upstream. This way we can
-ensure functionality is preserved across versions and covered by tests.
+A file hook runs on each event. You can filter events or projects
+in a file hook's code, and create many file hooks as you need. Each file hook is
+triggered by GitLab asynchronously in case of an event. For a list of events
+see the [system hooks](../system_hooks/system_hooks.md) documentation.
NOTE:
File hooks must be configured on the file system of the GitLab server. Only GitLab
@@ -24,10 +23,9 @@ server administrators can complete these tasks. Explore
[system hooks](../system_hooks/system_hooks.md) or [webhooks](../user/project/integrations/webhooks.md)
as an option if you do not have file system access.
-A file hook runs on each event. You can filter events or projects
-in a file hook's code, and create many file hooks as you need. Each file hook is
-triggered by GitLab asynchronously in case of an event. For a list of events
-see the [system hooks](../system_hooks/system_hooks.md) documentation.
+Instead of writing and supporting your own file hook, you can also make changes
+directly to the GitLab source code and contribute back upstream. In this way, we can
+ensure functionality is preserved across versions and covered by tests.
## Setup
@@ -67,7 +65,7 @@ message is logged to:
- `log/file_hook.log` in a source installation.
NOTE:
-Before 14.0 release, the file name was `plugin.log`
+In GitLab 13.12 and earlier, the filename was `plugin.log`
## Creating file hooks
diff --git a/doc/administration/geo/disaster_recovery/planned_failover.md b/doc/administration/geo/disaster_recovery/planned_failover.md
index a7a64701cb..6312ed669a 100644
--- a/doc/administration/geo/disaster_recovery/planned_failover.md
+++ b/doc/administration/geo/disaster_recovery/planned_failover.md
@@ -162,6 +162,9 @@ be disabled on the **primary** site:
## Finish replicating and verifying all data
+NOTE:
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which the Geo secondary site statuses will appear to stop updating and become unhealthy. For more information, see [Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](../replication/troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
1. If you are manually replicating any data not managed by Geo, trigger the
final replication process now.
1. On the **primary** node:
@@ -192,12 +195,13 @@ At this point, your **secondary** node contains an up-to-date copy of everything
## Promote the **secondary** node
-Finally, follow the [Disaster Recovery docs](index.md) to promote the
-**secondary** node to a **primary** node. This process causes a brief outage on the **secondary** node, and users may need to log in again.
+After the replication is finished, [promote the **secondary** node to a **primary** node](index.md). This process causes a brief outage on the **secondary** node, and users may need to log in again. If you follow the steps correctly, the old primary Geo site should still be disabled and user traffic should go to the newly-promoted site instead.
-Once it is completed, the maintenance window is over! Your new **primary** node, now
-begin to diverge from the old one. If problems do arise at this point, failing
+When the promotion is completed, the maintenance window is over, and your new **primary** node now
+begins to diverge from the old one. If problems do arise at this point, failing
back to the old **primary** node [is possible](bring_primary_back.md), but likely to result
in the loss of any data uploaded to the new **primary** in the meantime.
-Don't forget to remove the broadcast message after failover is complete.
+Don't forget to remove the broadcast message after the failover is complete.
+
+Finally, you can bring the [old site back as a secondary](bring_primary_back.md#configure-the-former-primary-node-to-be-a-secondary-node).
diff --git a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
index 4255fba83f..3eb7bc2a8e 100644
--- a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
+++ b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_multi_node.md
@@ -63,6 +63,9 @@ Before following any of those steps, make sure you have `root` access to the
**secondary** to promote it, since there isn't provided an automated way to
promote a Geo replica and perform a failover.
+NOTE:
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which the Geo secondary site statuses will appear to stop updating and become unhealthy. For more information, see [Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](../../replication/troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
On the **secondary** node:
1. On the top bar, select **Menu > Admin**.
diff --git a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_single_node.md b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_single_node.md
index 18923da105..d4782144df 100644
--- a/doc/administration/geo/disaster_recovery/runbooks/planned_failover_single_node.md
+++ b/doc/administration/geo/disaster_recovery/runbooks/planned_failover_single_node.md
@@ -51,6 +51,9 @@ Before following any of those steps, make sure you have `root` access to the
**secondary** to promote it, since there isn't provided an automated way to
promote a Geo replica and perform a failover.
+NOTE:
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which the Geo secondary site statuses will appear to stop updating and become unhealthy. For more information, see [Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](../../replication/troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
On the **secondary** node, navigate to the **Admin Area > Geo** dashboard to
review its status. Replicated objects (shown in green) should be close to 100%,
and there should be no failures (shown in red). If a large proportion of
diff --git a/doc/administration/geo/replication/datatypes.md b/doc/administration/geo/replication/datatypes.md
index 3f38436429..e8e87f9248 100644
--- a/doc/administration/geo/replication/datatypes.md
+++ b/doc/administration/geo/replication/datatypes.md
@@ -198,12 +198,12 @@ successfully, you must replicate their data using some other means.
|[Package Registry](../../../user/packages/package_registry/index.md) | **Yes** (13.2) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.10) | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_package_file_replication`, enabled by default. |
|[Versioned Terraform State](../../terraform_state.md) | **Yes** (13.5) | [**Yes**](#limitation-of-verification-for-files-in-object-storage) (13.12) | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_terraform_state_version_replication`, enabled by default. Verification was behind the feature flag `geo_terraform_state_version_verification`, which was removed in 14.0|
|[External merge request diffs](../../merge_request_diffs.md) | **Yes** (13.5) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Replication is behind the feature flag `geo_merge_request_diff_replication`, enabled by default. Verification is under development, behind the feature flag `geo_merge_request_diff_verification`, introduced in 14.0.|
-|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
-|[Server-side Git hooks](../../server_hooks.md) | [No](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | |
-|[Elasticsearch integration](../../../integration/elasticsearch.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | |
+|[Versioned snippets](../../../user/snippets.md#versioned-snippets) | [**Yes** (13.7)](https://gitlab.com/groups/gitlab-org/-/epics/2809) | [**Yes** (14.2)](https://gitlab.com/groups/gitlab-org/-/epics/2810) | No | Verification was implemented behind the feature flag `geo_snippet_repository_verification` in 13.11, and the feature flag was removed in 14.2. |
|[GitLab Pages](../../pages/index.md) | [**Yes** (14.3)](https://gitlab.com/groups/gitlab-org/-/epics/589) | No | Via Object Storage provider if supported. Native Geo support (Beta). | Behind feature flag `geo_pages_deployment_replication`, enabled by default. |
-|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [No](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked on [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for Disaster Recovery purposes because it can be recreated from external sources. |
-|[Vulnerability Export](../../../user/application_security/vulnerability_report/#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | | Not planned because they are ephemeral and sensitive. They can be regenerated on demand. |
+|[Server-side Git hooks](../../server_hooks.md) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/1867) | No | No | Not planned because of current implementation complexity, low customer interest, and availability of alternatives to hooks. |
+|[Elasticsearch integration](../../../integration/elasticsearch.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/1186) | No | No | Not planned because further product discovery is required and Elasticsearch (ES) clusters can be rebuilt. Secondaries currently use the same ES cluster as the primary. |
+|[Dependency proxy images](../../../user/packages/dependency_proxy/index.md) | [Not planned](https://gitlab.com/gitlab-org/gitlab/-/issues/259694) | No | No | Blocked by [Geo: Secondary Mimicry](https://gitlab.com/groups/gitlab-org/-/epics/1528). Replication of this cache is not needed for disaster recovery purposes because it can be recreated from external sources. |
+|[Vulnerability Export](../../../user/application_security/vulnerability_report/#export-vulnerability-details) | [Not planned](https://gitlab.com/groups/gitlab-org/-/epics/3111) | No | No | Not planned because they are ephemeral and sensitive information. They can be regenerated on demand. |
#### Limitation of verification for files in Object Storage
diff --git a/doc/administration/geo/replication/geo_validation_tests.md b/doc/administration/geo/replication/geo_validation_tests.md
index c6b1078ddf..a4c2f15621 100644
--- a/doc/administration/geo/replication/geo_validation_tests.md
+++ b/doc/administration/geo/replication/geo_validation_tests.md
@@ -114,6 +114,13 @@ The following are GitLab upgrade validation tests we performed.
The following are PostgreSQL upgrade validation tests we performed.
+### September 2021
+
+[Verify Geo installation with PostgreSQL 13](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6131):
+
+- Description: With PostgreSQL 13 available as an opt-in version in GitLab 14.1, we tested fresh installations of GitLab with Geo when PostgreSQL 13 is enabled.
+- Outcome: Successfully built an environment with Geo and PostgreSQL 13 using [GitLab Environment Toolkit](https://gitlab.com/gitlab-org/quality/gitlab-environment-toolkit) and performed Geo QA tests against the environment without failures.
+
### September 2020
[Verify PostgreSQL 12 upgrade for Geo installations](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/5454):
diff --git a/doc/administration/geo/replication/multiple_servers.md b/doc/administration/geo/replication/multiple_servers.md
index 7db210d31f..87b1aa7fc4 100644
--- a/doc/administration/geo/replication/multiple_servers.md
+++ b/doc/administration/geo/replication/multiple_servers.md
@@ -199,7 +199,7 @@ then make the following modifications:
## `application_role` already enables this. You only need this line if
## you selectively enable individual services that depend on Rails, like
- ## `puma`, `sidekiq`, `geo-logcursor`, etc.
+ ## `puma`, `sidekiq`, `geo-logcursor`, and so on.
gitlab_rails['enable'] = true
##
diff --git a/doc/administration/geo/replication/object_storage.md b/doc/administration/geo/replication/object_storage.md
index 1f799b3012..3a10d3bad5 100644
--- a/doc/administration/geo/replication/object_storage.md
+++ b/doc/administration/geo/replication/object_storage.md
@@ -73,7 +73,7 @@ GitLab does not currently support the case where both:
## Third-party replication services
When using Amazon S3, you can use
-[CRR](https://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) to
+[Cross-Region Replication (CRR)](https://docs.aws.amazon.com/AmazonS3/latest/dev/crr.html) to
have automatic replication between the bucket used by the **primary** site and
the bucket used by **secondary** sites.
diff --git a/doc/administration/geo/replication/security_review.md b/doc/administration/geo/replication/security_review.md
index 966902a3d7..df893298f8 100644
--- a/doc/administration/geo/replication/security_review.md
+++ b/doc/administration/geo/replication/security_review.md
@@ -26,7 +26,7 @@ from [owasp.org](https://owasp.org/).
- Geo streams almost all data held by a GitLab instance between sites. This
includes full database replication, most files (user-uploaded attachments,
- etc) and repository + wiki data. In a typical configuration, this will
+ and so on) and repository + wiki data. In a typical configuration, this will
happen across the public Internet, and be TLS-encrypted.
- PostgreSQL replication is TLS-encrypted.
- See also: [only TLSv1.2 should be supported](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/2948)
diff --git a/doc/administration/geo/replication/troubleshooting.md b/doc/administration/geo/replication/troubleshooting.md
index 7b82d742bd..b7370d3205 100644
--- a/doc/administration/geo/replication/troubleshooting.md
+++ b/doc/administration/geo/replication/troubleshooting.md
@@ -83,7 +83,7 @@ Checking Geo ... Finished
#### Sync status Rake task
Current sync information can be found manually by running this Rake task on any
-**secondary** app node:
+node running Rails (Puma, Sidekiq, or Geo Log Cursor) on the Geo **secondary** site:
```shell
sudo gitlab-rake geo:status
@@ -292,9 +292,8 @@ be set on the **primary** database. In GitLab 9.4, we have made this setting
default to 1. You may need to increase this value if you have more
**secondary** nodes.
-Be sure to restart PostgreSQL for this to take
-effect. See the [PostgreSQL replication
-setup](../setup/database.md#postgresql-replication) guide for more details.
+Be sure to restart PostgreSQL for this to take effect. See the
+[PostgreSQL replication setup](../setup/database.md#postgresql-replication) guide for more details.
### Message: `FATAL: could not start WAL streaming: ERROR: replication slot "geo_secondary_my_domain_com" does not exist`?
@@ -430,7 +429,7 @@ their resync may take a long time and cause significant load on your Geo nodes,
storage and network systems.
If you get the error `Synchronization failed - Error syncing repository` along with the following log messages, this indicates that the expected `geo` remote is not present in the `.git/config` file
-of a repository on the secondary Geo node's filesystem:
+of a repository on the secondary Geo node's file system:
```json
{
@@ -803,7 +802,7 @@ get_ctl_options': invalid option: --skip-preflight-checks (OptionParser::Invalid
get_ctl_options': invalid option: --force (OptionParser::InvalidOption)
```
-This can happen with XFS or filesystems that list files in lexical order, because the
+This can happen with XFS or file systems that list files in lexical order, because the
load order of the Omnibus command files can be different than expected, and a global function would get redefined.
More details can be found in [the related issue](https://gitlab.com/gitlab-org/omnibus-gitlab/-/issues/6076).
@@ -923,6 +922,14 @@ To resolve this issue:
If using a load balancer, ensure that the load balancer's URL is set as the `external_url` in the
`/etc/gitlab/gitlab.rb` of the nodes behind the load balancer.
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+In GitLab 13.9 through GitLab 14.3, when [GitLab Maintenance Mode](../../maintenance_mode/index.md) is enabled, the status of Geo secondary sites will stop getting updated. After 10 minutes, the status will become `Unhealthy`.
+
+Geo secondary sites will continue to replicate and verify data, and the secondary sites should still be usable. You can use the [Sync status Rake task](#sync-status-rake-task) to determine the actual status of a secondary site during Maintenance Mode.
+
+This bug was [fixed in GitLab 14.4](https://gitlab.com/gitlab-org/gitlab/-/issues/292983).
+
### GitLab Pages return 404 errors after promoting
This is due to [Pages data not being managed by Geo](datatypes.md#limitations-on-replicationverification).
diff --git a/doc/administration/geo/replication/version_specific_updates.md b/doc/administration/geo/replication/version_specific_updates.md
index 84193e6baa..1b22a5f099 100644
--- a/doc/administration/geo/replication/version_specific_updates.md
+++ b/doc/administration/geo/replication/version_specific_updates.md
@@ -13,6 +13,8 @@ for updating Geo nodes.
## Updating to 14.1, 14.2, 14.3
+### Multi-arch images
+
We found an [issue](https://gitlab.com/gitlab-org/gitlab/-/issues/336013) where the Container Registry replication wasn't fully working if you used multi-arch images. In case of a multi-arch image, only the primary architecture (for example `amd64`) would be replicated to the secondary node. This has been [fixed in GitLab 14.3](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/67624) and was backported to 14.2 and 14.1, but manual steps are required to force a re-sync.
You can check if you are affected by running:
@@ -46,18 +48,28 @@ Otherwise, on all your **secondary** nodes, in a [Rails console](../../operation
If you are running a version prior to 14.1 and are using Geo and multi-arch containers in your Container Registry, we recommend [upgrading](updating_the_geo_sites.md) to at least GitLab 14.1.
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
## Updating to GitLab 14.0/14.1
+### Primary sites can not be removed from the UI
+
We found an issue where [Primary sites can not be removed from the UI](https://gitlab.com/gitlab-org/gitlab/-/issues/338231).
This bug only exists in the UI and does not block the removal of Primary sites using any other method.
-### If you have already updated to an affected version and need to remove your Primary site
+If you are running an affected version and need to remove your Primary site, you can manually remove the Primary site by using the [Geo Nodes API](../../../api/geo_nodes.md#delete-a-geo-node).
-You can manually remove the Primary site by using the [Geo Nodes API](../../../api/geo_nodes.md#delete-a-geo-node).
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
## Updating to GitLab 13.12
+### Secondary nodes re-download all LFS files upon update
+
We found an issue where [secondary nodes re-download all LFS files](https://gitlab.com/gitlab-org/gitlab/-/issues/334550) upon update. This bug:
- Only applies to Geo secondary sites that have replicated LFS objects.
@@ -68,7 +80,7 @@ We found an issue where [secondary nodes re-download all LFS files](https://gitl
If you don't have many LFS objects or can stand a bit of churn, then it is safe to let the secondary sites re-download LFS objects.
If you do have many LFS objects, or many Geo secondary sites, or limited bandwidth, or a combination of them all, then we recommend you skip GitLab 13.12.0 through 13.12.6 and update to GitLab 13.12.7 or newer.
-### If you have already updated to an affected version, and the re-sync is ongoing
+#### If you have already updated to an affected version, and the re-sync is ongoing
You can manually migrate the legacy sync state to the new state column by running the following command in a [Rails console](../../operations/rails_console.md). It should take under a minute:
@@ -76,15 +88,31 @@ You can manually migrate the legacy sync state to the new state column by runnin
Geo::LfsObjectRegistry.where(state: 0, success: true).update_all(state: 2)
```
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
## Updating to GitLab 13.11
We found an [issue with Git clone/pull through HTTP(s)](https://gitlab.com/gitlab-org/gitlab/-/issues/330787) on Geo secondaries and on any GitLab instance if maintenance mode is enabled. This was caused by a regression in GitLab Workhorse. This is fixed in the [GitLab 13.11.4 patch release](https://about.gitlab.com/releases/2021/05/14/gitlab-13-11-4-released/). To avoid this issue, upgrade to GitLab 13.11.4 or later.
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
+## Updating to GitLab 13.10
+
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
## Updating to GitLab 13.9
+### Error during zero-downtime update: "cannot drop column asset_proxy_whitelist"
+
We've detected an issue [with a column rename](https://gitlab.com/gitlab-org/gitlab/-/issues/324160)
that will prevent upgrades to GitLab 13.9.0, 13.9.1, 13.9.2 and 13.9.3 when following the zero-downtime steps. It is necessary
-to perform the following additional steps for the zero-downtime upgrade:
+to perform the following additional steps for the zero-downtime update:
1. Before running the final `sudo gitlab-rake db:migrate` command on the deploy node,
execute the following queries using the PostgreSQL console (or `sudo gitlab-psql`)
@@ -118,6 +146,10 @@ DETAIL: trigger trigger_0d588df444c8 on table application_settings depends on co
To work around this bug, follow the previous steps to complete the update.
More details are available [in this issue](https://gitlab.com/gitlab-org/gitlab/-/issues/324160).
+### Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode
+
+GitLab 13.9 through GitLab 14.3 are affected by a bug in which enabling [GitLab Maintenance Mode](../../maintenance_mode/index.md) will cause Geo secondary site statuses to appear to stop updating and become unhealthy. For more information, see [Troubleshooting - Geo Admin Area shows 'Unhealthy' after enabling Maintenance Mode](troubleshooting.md#geo-admin-area-shows-unhealthy-after-enabling-maintenance-mode).
+
## Updating to GitLab 13.7
We've detected an issue with the `FetchRemove` call used by Geo secondaries.
diff --git a/doc/administration/get_started.md b/doc/administration/get_started.md
index c0d5a45d8d..2455aecafe 100644
--- a/doc/administration/get_started.md
+++ b/doc/administration/get_started.md
@@ -54,7 +54,7 @@ Get started:
You may need to import projects from external sources like GitHub, Bitbucket, or another instance of GitLab. Many external sources can be imported into GitLab.
- Review the [GitLab projects documentation](../user/project/index.md#project-integrations).
-- Consider [repository mirroring](../user/project/repository/repository_mirroring.md)—an [alternative to project migrations](../ci/ci_cd_for_external_repos/index.md).
+- Consider [repository mirroring](../user/project/repository/mirror/index.md)—an [alternative to project migrations](../ci/ci_cd_for_external_repos/index.md).
- Check out our [migration index](../user/project/import/index.md) for documentation on common migration paths.
- Schedule your project exports with our [import/export API](../api/project_import_export.md#schedule-an-export).
@@ -128,7 +128,7 @@ The routine differs, depending on whether you deployed with Omnibus or the Helm
When you backing up an Omnibus (single node) GitLab server, you can use a single Rake task.
-Learn about [backing up Omnibus or Helm variations](../raketasks/backup_restore.md#back-up-gitlab).
+Learn about [backing up Omnibus or Helm variations](../raketasks/backup_restore.md).
This process backs up your entire instance, but does not back up the configuration files. Ensure those are backed up separately.
Keep your configuration files and backup archives in a separate location to ensure the encryption keys are not kept with the encrypted data.
@@ -214,7 +214,7 @@ If you use GitLab SaaS, you have several channels with which to get support and
To get assistance for GitLab SaaS:
-- Access [GitLab Docs](../README.md) for self-service support.
+- Access [GitLab Docs](../index.md) for self-service support.
- Join the [GitLab Forum](https://forum.gitlab.com/) for community support.
- Gather [your subscription information](https://about.gitlab.com/support/#for-self-managed-users) before submitting a ticket.
- Submit a support ticket for:
diff --git a/doc/administration/gitaly/faq.md b/doc/administration/gitaly/faq.md
index c7ecaa020e..f79b9555c1 100644
--- a/doc/administration/gitaly/faq.md
+++ b/doc/administration/gitaly/faq.md
@@ -35,7 +35,7 @@ For more information, see:
## Are there instructions for migrating to Gitaly Cluster?
-Yes! For more information, see [Migrate to Gitaly Cluster](index.md#migrate-to-gitaly-cluster).
+Yes! For more information, see [Migrating to Gitaly Cluster](index.md#migrating-to-gitaly-cluster).
## What are some repository storage recommendations?
diff --git a/doc/administration/gitaly/index.md b/doc/administration/gitaly/index.md
index 7a7aac884e..c689530e12 100644
--- a/doc/administration/gitaly/index.md
+++ b/doc/administration/gitaly/index.md
@@ -10,14 +10,22 @@ type: reference
[Gitaly](https://gitlab.com/gitlab-org/gitaly) provides high-level RPC access to Git repositories.
It is used by GitLab to read and write Git data.
+Gitaly is present in every GitLab installation and coordinates Git repository
+storage and retrieval. Gitaly can be:
+
+- A simple background service operating on a single instance Omnibus GitLab (all of
+ GitLab on one machine).
+- Separated onto its own instance and configured in a full cluster configuration,
+ depending on scaling and availability requirements.
+
Gitaly implements a client-server architecture:
- A Gitaly server is any node that runs Gitaly itself.
-- A Gitaly client is any node that runs a process that makes requests of the Gitaly server. These
- include, but are not limited to:
- - [GitLab Rails application](https://gitlab.com/gitlab-org/gitlab).
- - [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell).
- - [GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse).
+- A Gitaly client is any node that runs a process that makes requests of the Gitaly server. Gitaly clients are also known as _Gitaly consumers_ and include:
+ - [GitLab Rails application](https://gitlab.com/gitlab-org/gitlab)
+ - [GitLab Shell](https://gitlab.com/gitlab-org/gitlab-shell)
+ - [GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse)
+ - [GitLab Elasticsearch Indexer](https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer)
Gitaly manages only Git repository access for GitLab. Other types of GitLab data aren't accessed
using Gitaly.
@@ -35,9 +43,49 @@ repository storage is either:
- Read requests are distributed between multiple Gitaly nodes, which can improve performance.
- Write requests are broadcast to repository replicas.
-WARNING:
-Engineering support for NFS for Git repositories is deprecated. Read the
-[deprecation notice](#nfs-deprecation-notice).
+## Guidance regarding Gitaly Cluster
+
+Gitaly Cluster provides the benefits of fault tolerance, but comes with additional complexity of setup and management. Please review existing technical limitations and considerations prior to deploying Gitaly Cluster.
+
+- [Known issues](#known-issues)
+- [Snapshot limitations](#snapshot-backup-and-recovery-limitations).
+
+Please also review the [configuration guidance](configure_gitaly.md) and [Repository storage options](../repository_storage_paths.md) to make sure that Gitaly Cluster is the best set-up for you. Finally, refer to the following guidance:
+
+- If you have not yet migrated to Gitaly Cluster and want to continue using NFS, remain on the
+ service you are using. NFS is supported in 14.x releases.
+- If you have not yet migrated to Gitaly Cluster but want to migrate away from NFS, you have two options - a sharded Gitaly instance or Gitaly Cluster.
+- If you have migrated to Gitaly Cluster and the limitations and tradeoffs are not suitable for your environment, your options are:
+ 1. [Migrate off Gitaly Cluster](#migrate-off-gitaly-cluster) back to your NFS solution
+ 1. [Migrate off Gitaly Cluster](#migrate-off-gitaly-cluster) to NFS solution or to a sharded Gitaly instance.
+
+Reach out to your Technical Account Manager or customer support if you have any questions.
+
+### Known issues
+
+The following table outlines current known issues impacting the use of Gitaly Cluster. For
+the current status of these issues, please refer to the referenced issues and epics.
+
+| Issue | Summary | How to avoid |
+|:--------------------------------------------------------------------------------------|:------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------|:--------------------------------|
+| Gitaly Cluster + Geo - Issues retrying failed syncs | If Gitaly Cluster is used on a Geo secondary site, repositories that have failed to sync could continue to fail when Geo tries to resync them. Recovering from this state requires assistance from support to run manual steps. Work is in-progress to update Gitaly Cluster to [identify repositories with a unique and persistent identifier](https://gitlab.com/gitlab-org/gitaly/-/issues/3485), which is expected to resolve the issue. | No known solution at this time. |
+| Database inconsistencies due to repository access outside of Gitaly Cluster's control | Operations that write to the repository storage that do not go through normal Gitaly Cluster methods can cause database inconsistencies. These can include (but are not limited to) snapshot restoration for cluster node disks, node upgrades which modify files under Git control, or any other disk operation that may touch repository storage external to GitLab. The Gitaly team is actively working to provide manual commands to [reconcile the Praefect database with the repository storage](https://gitlab.com/groups/gitlab-org/-/epics/6723). | Don't directly change repositories on any Gitaly Cluster node at this time. |
+| Praefect unable to insert data into the database due to migrations not being applied after an upgrade | If the database is not kept up to date with completed migrations, then the Praefect node is unable to perform normal operation. | Make sure the Praefect database is up and running with all migrations completed (For example: `/opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml sql-migrate-status` should show a list of all applied migrations). Consider [requesting live upgrade assistance](https://about.gitlab.com/support/scheduling-live-upgrade-assistance.html) so your upgrade plan can be reviewed by support. |
+| Restoring a Gitaly Cluster node from a snapshot in a running cluster | Because the Gitaly Cluster runs with consistent state, introducing a single node that is behind will result in the cluster not being able to reconcile the nodes data and other nodes data | Don't restore a single Gitaly Cluster node from a backup snapshot. If you need to restore from backup, it's best to snapshot all Gitaly Cluster nodes at the same time and take a database dump of the Praefect database. |
+
+### Snapshot backup and recovery limitations
+
+Gitaly Cluster does not support snapshot backups because these can cause issues where the Praefect
+database becomes out of sync with the disk storage. Because of how Praefect rebuilds the replication
+metadata of Gitaly disk information during a restore, we recommend using the
+[official backup and restore Rake tasks](../../raketasks/backup_restore.md). If you are unable to use this method, please contact customer support for restoration help.
+
+To track progress on work on a solution for manually re-synchronizing the Praefect database with
+disk storage, see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6575).
+
+### What to do if you are on Gitaly Cluster experiencing an issue or limitation
+
+Please contact customer support for immediate help in restoration or recovery.
## Gitaly
@@ -156,54 +204,6 @@ WARNING:
If complete cluster failure occurs, disaster recovery plans should be executed. These can affect the
RPO and RTO discussed above.
-### Architecture and configuration recommendations
-
-The following table provides recommendations based on your
-requirements. Users means concurrent users actively performing
-simultaneous Git Operations.
-
-Gitaly services are present in every GitLab installation and always coordinates Git repository storage and
-retrieval. Gitaly can be as simple as a single background service when operating on a single instance Omnibus
-GitLab (All of GitLab on one machine). Gitaly can be separated into it's own instance and it can be configured in
-a full cluster configuration depending on scaling and availability requirements.
-
-The GitLab Reference Architectures provide guidance for what Gitaly configuration is advisable at each of the scales.
-The Gitaly configuration is noted by the architecture diagrams and the table of required resources.
-
-| User Scaling | Reference Architecture To Use | GitLab Instance Configuration | Gitaly Configuration | Git Repository Storage | Instances Dedicated to Gitaly Services |
-| ------------------------------------------------------------ | ------------------------------------------------------------ | -------------------------------------------- | ------------------------------- | ---------------------------------- | -------------------------------------- |
-| Up to 1000 Users | [1K](../reference_architectures/1k_users.md) | Single Instance for all of GitLab | Already Integrated as a Service | Local Disk | 0 |
-| Up to 2999 Users | [2K](../reference_architectures/2k_users.md) | Horizontally Scaled GitLab Instance (Non-HA) | Single Gitaly Server | Local Disk of Gitaly Instance | 1 |
-| 3000 Users and Over | [3K](../reference_architectures/1k_users.md) | Horizontally Scaled GitLab Instance with HA | Gitaly Cluster | Local Disk of Each Gitaly Instance | 8 |
-| RTO/RPO Requirements for AZ Failure Tolerance Regardless of User Scale | [3K (with downscaling)](../reference_architectures/3k_users.md) | Custom (1) | Gitaly Cluster | Local Disk of Each Gitaly Instance | 8 |
-
-1. If you feel that you need AZ Failure Tolerance for user scaling lower than 3K, please contact Customer Success
- to discuss your RTO and RPO needs and what options exist to meet those objectives.
-
-WARNING:
-At present, some [known database inconsistency issues](#known-issues-impacting-gitaly-cluster)
-exist in Gitaly Cluster. It is our recommendation that for now, you remain on your current service.
-We will adjust the date for NFS support removal if this applies to you.
-
-### Known issues impacting Gitaly Cluster
-
-The following table outlines current known issues impacting the use of Gitaly Cluster. For
-the most up to date status of these issues, please refer to the referenced issues / epics.
-
-| Issue | Summary |
-| Gitaly Cluster + Geo can cause database inconsistencies | There are some conditions during Geo replication that can cause database inconsistencies with Gitaly Cluster. These have been identified and are being resolved by updating Gitaly Cluster to [identify repositories with a unique and persistent identifier](https://gitlab.com/gitlab-org/gitaly/-/issues/3485). |
-| Database inconsistencies due to repository access outside of Gitaly Cluster's control | Operations that write to the repository storage which do not go through normal Gitaly Cluster methods can cause database inconsistencies. These can include (but are not limited to) snapshot restoration for cluster node disks, node upgrades which modify files under Git control, or any other disk operation that may touch repository storage external to GitLab. The Gitaly team is actively working to provide manual commands to [reconcile the Praefect database with the repository storage](https://gitlab.com/groups/gitlab-org/-/epics/6723). |
-
-### Snapshot backup and recovery limitations
-
-Gitaly Cluster does not support snapshot backups because these can cause issues where the
-Praefect database becomes out of sync with the disk storage. Because of how Praefect rebuilds
-the replication metadata of Gitaly disk information during a restore, we recommend using the
-[official backup and restore Rake tasks](../../raketasks/backup_restore.md).
-
-To track progress on work on a solution for manually re-synchronizing the Praefect database
-with disk storage, see [this epic](https://gitlab.com/groups/gitlab-org/-/epics/6575).
-
### Virtual storage
Virtual storage makes it viable to have a single repository storage in GitLab to simplify repository
@@ -232,9 +232,7 @@ As with normal Gitaly storages, virtual storages can be sharded.
### Moving beyond NFS
-WARNING:
-Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be
-unavailable from GitLab 15.0. No further enhancements are planned for this feature.
+Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be unavailable starting GitLab 15.0. Please see our [statement of support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs) for more details.
[Network File System (NFS)](https://en.wikipedia.org/wiki/Network_File_System)
is not well suited to Git workloads which are CPU and IOPS sensitive.
@@ -355,13 +353,9 @@ For configuration information, see [Configure replication factor](praefect.md#co
For more information on configuring Gitaly Cluster, see [Configure Gitaly Cluster](praefect.md).
-## Migrate to Gitaly Cluster
+## Migrating to Gitaly Cluster
-We recommend you migrate to Gitaly Cluster if your
-[requirements recommend](#architecture-and-configuration-recommendations) Gitaly Cluster.
-
-Whether migrating to Gitaly Cluster because of [NFS support deprecation](index.md#nfs-deprecation-notice)
-or to move from single Gitaly nodes, the basic process involves:
+Please see [current guidance on Gitaly Cluster](#guidance-regarding-gitaly-cluster). The basic process for migrating to Gitaly Cluster involves:
1. Create the required storage. Refer to
[repository storage recommendations](faq.md#what-are-some-repository-storage-recommendations).
@@ -371,9 +365,20 @@ or to move from single Gitaly nodes, the basic process involves:
automatic migration but the moves can be scheduled with the GitLab API.
WARNING:
-At present, some [known database inconsistency issues](#known-issues-impacting-gitaly-cluster)
-exist in Gitaly Cluster. It is our recommendation that for now, you remain on your current service.
-We will adjust the date for NFS support removal if this applies to you.
+Some [known database inconsistency issues](#known-issues) exist in Gitaly Cluster. We recommend you
+remain on your current service for now.
+
+### Migrate off Gitaly Cluster
+
+If you have repositories stored on a Gitaly Cluster, but you'd like to migrate
+them back to direct Gitaly storage:
+
+1. Create and configure a new
+ [Gitaly server](configure_gitaly.md#run-gitaly-on-its-own-server).
+1. [Move the repositories](../operations/moving_repositories.md#move-repositories)
+ to the newly created storage. There are different possibilities to move them
+ by shard or by group, this gives you the opportunity to spread them over
+ multiple Gitaly servers.
## Monitor Gitaly and Gitaly Cluster
@@ -615,20 +620,4 @@ The second facet presents the only real solution. For this, we developed
## NFS deprecation notice
Engineering support for NFS for Git repositories is deprecated. Technical support is planned to be
-unavailable from GitLab 15.0. No further enhancements are planned for this feature.
-
-Additional information:
-
-- [Recommended NFS mount options and known issues with Gitaly and NFS](../nfs.md#upgrade-to-gitaly-cluster-or-disable-caching-if-experiencing-data-loss).
-- [GitLab statement of support](https://about.gitlab.com/support/statement-of-support.html#gitaly-and-nfs).
-
-GitLab recommends:
-
-- Creating a [Gitaly Cluster](#gitaly-cluster) as soon as possible.
-- [Moving your repositories](#migrate-to-gitaly-cluster) from NFS-based storage to Gitaly
- Cluster.
-
-We welcome your feedback on this process. You can:
-
-- Raise a support ticket.
-- [Comment on the epic](https://gitlab.com/groups/gitlab-org/-/epics/4916).
+unavailable from GitLab 15.0. For further information, please see our [NFS Deprecation](../nfs.md#gitaly-and-nfs-deprecation) documentation.
diff --git a/doc/administration/gitaly/praefect.md b/doc/administration/gitaly/praefect.md
index eb666f1caf..d3ea71bc8d 100644
--- a/doc/administration/gitaly/praefect.md
+++ b/doc/administration/gitaly/praefect.md
@@ -265,8 +265,8 @@ praefect['database_direct_dbname'] = 'praefect_production'
#praefect['database_direct_sslrootcert'] = '...'
```
-We recommend using PgBouncer with `session` pool mode instead. You can use the [bundled
-PgBouncer](../postgresql/pgbouncer.md) or use an external PgBouncer and [configure it
+We recommend using PgBouncer with `session` pool mode instead. You can use the
+[bundled PgBouncer](../postgresql/pgbouncer.md) or use an external PgBouncer and [configure it
manually](https://www.pgbouncer.org/config.html).
The following example uses the bundled PgBouncer and sets up two separate connection pools,
@@ -429,7 +429,7 @@ On the **Praefect** node:
WARNING:
If you have data on an already existing storage called
`default`, you should configure the virtual storage with another name and
- [migrate the data to the Gitaly Cluster storage](index.md#migrate-to-gitaly-cluster)
+ [migrate the data to the Gitaly Cluster storage](index.md#migrating-to-gitaly-cluster)
afterwards.
Replace `PRAEFECT_INTERNAL_TOKEN` with a strong secret, which is used by
@@ -475,8 +475,8 @@ On the **Praefect** node:
1. [Introduced](https://gitlab.com/groups/gitlab-org/-/epics/2013) in GitLab 13.1 and later, enable [distribution of reads](index.md#distributed-reads).
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
- Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Save the changes to `/etc/gitlab/gitlab.rb` and
+ [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
```shell
gitlab-ctl reconfigure
@@ -499,16 +499,16 @@ On the **Praefect** node:
running reconfigure automatically when running commands such as `apt-get update`. This way any
additional configuration changes can be done and then reconfigure can be run manually.
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
- Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Save the changes to `/etc/gitlab/gitlab.rb` and
+ [reconfigure Praefect](../restart_gitlab.md#omnibus-gitlab-reconfigure):
```shell
gitlab-ctl reconfigure
```
1. To ensure that Praefect [has updated its Prometheus listen
- address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734), [restart
- Praefect](../restart_gitlab.md#omnibus-gitlab-restart):
+ address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734),
+ [restart Praefect](../restart_gitlab.md#omnibus-gitlab-restart):
```shell
gitlab-ctl restart praefect
@@ -695,8 +695,8 @@ Particular attention should be shown to:
was set in the [previous section](#praefect). This document uses `gitaly-1`,
`gitaly-2`, and `gitaly-3` as Gitaly storage names.
-For more information on Gitaly server configuration, see our [Gitaly
-documentation](configure_gitaly.md#configure-gitaly-servers).
+For more information on Gitaly server configuration, see our
+[Gitaly documentation](configure_gitaly.md#configure-gitaly-servers).
1. SSH into the **Gitaly** node and login as root:
@@ -803,16 +803,16 @@ documentation](configure_gitaly.md#configure-gitaly-servers).
})
```
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
- Gitaly](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Save the changes to `/etc/gitlab/gitlab.rb` and
+ [reconfigure Gitaly](../restart_gitlab.md#omnibus-gitlab-reconfigure):
```shell
gitlab-ctl reconfigure
```
1. To ensure that Gitaly [has updated its Prometheus listen
- address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734), [restart
- Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
+ address](https://gitlab.com/gitlab-org/gitaly/-/issues/2734),
+ [restart Gitaly](../restart_gitlab.md#omnibus-gitlab-restart):
```shell
gitlab-ctl restart gitaly
@@ -893,7 +893,7 @@ Particular attention should be shown to:
WARNING:
If you have existing data stored on the default Gitaly storage,
- you should [migrate the data your Gitaly Cluster storage](index.md#migrate-to-gitaly-cluster)
+ you should [migrate the data your Gitaly Cluster storage](index.md#migrating-to-gitaly-cluster)
first.
```ruby
@@ -1044,8 +1044,8 @@ To get started quickly:
grafana['disable_login_form'] = false
```
-1. Save the changes to `/etc/gitlab/gitlab.rb` and [reconfigure
- GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure):
+1. Save the changes to `/etc/gitlab/gitlab.rb` and
+ [reconfigure GitLab](../restart_gitlab.md#omnibus-gitlab-reconfigure):
```shell
gitlab-ctl reconfigure
@@ -1309,12 +1309,7 @@ To minimize data loss in GitLab 13.0 to 14.0, Gitaly Cluster:
new primary. If the failed primary contained unreplicated writes, [data loss can occur](#check-for-data-loss).
> - Removed in GitLab 14.1. Instead, repositories [become unavailable](#unavailable-repositories).
-In GitLab 13.0 to 14.0, when Gitaly Cluster switches to a new primary, repositories enter
-read-only mode if they are out of date. This can happen after failing over to an outdated
-secondary. Read-only mode eases data recovery efforts by preventing writes that may conflict
-with the unreplicated writes on other nodes.
-
-When Gitaly Cluster switches to a new primary In GitLab 13.0 to 14.0, repositories enter
+When Gitaly Cluster switches to a new primary in GitLab 13.0 to 14.0, repositories enter
read-only mode if they are out of date. This can happen after failing over to an outdated
secondary. Read-only mode eases data recovery efforts by preventing writes that may conflict
with the unreplicated writes on other nodes.
@@ -1586,13 +1581,99 @@ all state associated with a given repository including:
sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml remove-repository -virtual-storage -repository
```
-- `-virtual-storage` is the virtual storage the repository is located in.
-- `-repository` is the repository's relative path in the storage.
+- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following:
-Sometimes parts of the repository continue to exist after running `remove-repository`. This can be caused
-because of:
+ ```ruby
+ praefect['virtual_storages'] = {
+ 'default' => {
+ ...
+ },
+ 'storage-1' => {
+ ...
+ }
+ }
+ ```
+
+ In this example, the virtual storage to specify is `default` or `storage-1`.
+
+- `-repository` is the repository's relative path in the storage [beginning with `@hashed`](../repository_storage_types.md#hashed-storage).
+ For example:
+
+ ```plaintext
+ @hashed/f5/ca/f5ca38f748a1d6eaf726b8a42fb575c3c71f1864a8143301782de13da2d9202b.git
+ ```
+
+Parts of the repository can continue to exist after running `remove-repository`. This can be because of:
- A deletion error.
- An in-flight RPC call targeting the repository.
If this occurs, run `remove-repository` again.
+
+### Manually list untracked repositories
+
+> [Introduced](https://gitlab.com/gitlab-org/gitaly/-/merge_requests/3926) in GitLab 14.4.
+
+The `list-untracked-repositories` Praefect sub-command lists repositories of the Gitaly Cluster that both:
+
+- Exist for at least one Gitaly storage.
+- Aren't tracked in the Praefect database.
+
+The command outputs:
+
+- Result to `STDOUT` and the command's logs.
+- Errors to `STDERR`.
+
+Each entry is a complete JSON string with a newline at the end (configurable using the
+`-delimiter` flag). For example:
+
+```plaintext
+sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml list-untracked-repositories
+{"virtual_storage":"default","storage":"gitaly-1","relative_path":"@hashed/ab/cd/abcd123456789012345678901234567890123456789012345678901234567890.git"}
+{"virtual_storage":"default","storage":"gitaly-1","relative_path":"@hashed/ab/cd/abcd123456789012345678901234567890123456789012345678901234567891.git"}
+```
+
+### Manually track repositories
+
+> [Introduced](https://gitlab.com/gitlab-org/omnibus-gitlab/-/merge_requests/5658) in GitLab 14.4.
+
+The `track-repository` Praefect sub-command adds repositories on disk to the Praefect database to be tracked.
+
+```shell
+sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config.toml track-repository -virtual-storage -repository
+```
+
+- `-virtual-storage` is the virtual storage the repository is located in. Virtual storages are configured in `/etc/gitlab/gitlab.rb` under `praefect['virtual_storages]` and looks like the following:
+
+ ```ruby
+ praefect['virtual_storages'] = {
+ 'default' => {
+ ...
+ },
+ 'storage-1' => {
+ ...
+ }
+ }
+ ```
+
+ In this example, the virtual storage to specify is `default` or `storage-1`.
+
+- `-repository` is the repository's relative path in the storage [beginning with `@hashed`](../repository_storage_types.md#hashed-storage).
+ For example:
+
+ ```plaintext
+ @hashed/f5/ca/f5ca38f748a1d6eaf726b8a42fb575c3c71f1864a8143301782de13da2d9202b.git
+ ```
+
+- `-authoritative-storage` is the storage we want Praefect to treat as the primary. Required if
+ [per-repository replication](#configure-replication-factor) is set as the replication strategy.
+
+The command outputs:
+
+- Results to `STDOUT` and the command's logs.
+- Errors to `STDERR`.
+
+This command fails if:
+
+- The repository is already being tracked by the Praefect database.
+- The repository does not exist on disk.
diff --git a/doc/administration/gitaly/troubleshooting.md b/doc/administration/gitaly/troubleshooting.md
index 1b53a0994f..1f90ebb756 100644
--- a/doc/administration/gitaly/troubleshooting.md
+++ b/doc/administration/gitaly/troubleshooting.md
@@ -243,7 +243,7 @@ To mirror-push branches and tags only, and avoid attempting to mirror-push prote
git push origin +refs/heads/*:refs/heads/* +refs/tags/*:refs/tags/*
```
-Any other namespaces that the admin wants to push can be included there as well via additional patterns.
+Any other namespaces that the administrator wants to push can be included there as well via additional patterns.
### Command line tools cannot connect to Gitaly
@@ -365,6 +365,15 @@ To determine the current primary Gitaly node for a specific Praefect node:
curl localhost:9652/metrics | grep gitaly_praefect_primaries`
```
+### Check that repositories are in sync
+
+Is [some cases](index.md#known-issues) the Praefect database can get out of sync with the underlying Gitaly nodes. To check that
+a given repository is fully synced on all nodes, run the [`gitlab:praefect:replicas` Rake task](../raketasks/praefect.md#replica-checksums)
+that checksums the repository on all Gitaly nodes.
+
+The [Praefect dataloss](praefect.md#check-for-data-loss) command only checks the state of the repo in the Praefect database, and cannot
+be relied to detect sync problems in this scenario.
+
### Relation does not exist errors
By default Praefect database tables are created automatically by `gitlab-ctl reconfigure` task.
@@ -393,7 +402,7 @@ $ sudo /opt/gitlab/embedded/bin/praefect -config /var/opt/gitlab/praefect/config
praefect sql-migrate: OK (applied 21 migrations)
```
-### Requests fail with 'repo scoped: invalid Repository' errors
+### Requests fail with 'repository scoped: invalid Repository' errors
This indicates that the virtual storage name used in the
[Praefect configuration](praefect.md#praefect) does not match the storage name used in
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 9412994edb..ee17edc35f 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -41,7 +41,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
### Configuring GitLab
-- [Adjust your instance's timezone](timezone.md): Customize the default time zone of GitLab.
+- [Adjust your instance's time zone](timezone.md): Customize the default time zone of GitLab.
- [System hooks](../system_hooks/system_hooks.md): Notifications when users, projects and keys are changed.
- [Security](../security/index.md): Learn what you can do to further secure your GitLab instance.
- [Usage statistics, version check, and Service Ping](../user/admin_area/settings/usage_statistics.md): Enable or disable information about your instance to be sent to GitLab, Inc.
diff --git a/doc/administration/instance_limits.md b/doc/administration/instance_limits.md
index 24ffee088f..a2729e6054 100644
--- a/doc/administration/instance_limits.md
+++ b/doc/administration/instance_limits.md
@@ -88,6 +88,29 @@ requests per user. For more information, read
- **Default rate limit**: Disabled by default.
+### Files API
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68561) in GitLab 14.3.
+
+FLAG:
+On self-managed GitLab, by default this feature is not available. To make it available,
+ask an administrator to [enable the `files_api_throttling` flag](../administration/feature_flags.md). On GitLab.com, this feature is available but can be configured by GitLab.com administrators only.
+The feature is not ready for production use.
+
+This setting limits the request rate on the Packages API per user or IP address. For more information, read
+[Files API rate limits](../user/admin_area/settings/files_api_rate_limits.md).
+
+- **Default rate limit**: Disabled by default.
+
+### Deprecated API endpoints
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/68645) in GitLab 14.4.
+
+This setting limits the request rate on deprecated API endpoints per user or IP address. For more information, read
+[Deprecated API rate limits](../user/admin_area/settings/deprecated_api_rate_limits.md).
+
+- **Default rate limit**: Disabled by default.
+
### Import/Export
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/35728) in GitLab 13.2.
@@ -212,7 +235,7 @@ Activity history for projects and individuals' profiles was limited to one year
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/14939) in GitLab 12.7.
-There is a limit when embedding metrics in GFM for performance reasons.
+There is a limit when embedding metrics in GitLab Flavored Markdown (GFM) for performance reasons.
- **Max limit**: 100 embeds.
@@ -240,10 +263,10 @@ Set the limit to `0` to disable it.
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/237891) in GitLab 13.7.
-The [minimum wait time between pull refreshes](../user/project/repository/repository_mirroring.md)
+The [minimum wait time between pull refreshes](../user/project/repository/mirror/index.md)
defaults to 300 seconds (5 minutes). For example, by default a pull refresh will only run once in a given 300 second period regardless of how many times you try to trigger it.
-This setting applies in the context of pull refreshes invoked via the [projects API](../api/projects.md#start-the-pull-mirroring-process-for-a-project), or when forcing an update by selecting the **Update now** (**{retry}**) button within **Settings > Repository > Mirroring repositories**. This setting has no effect on the automatic 30 minute interval schedule used by Sidekiq for [pull mirroring](../user/project/repository/repository_mirroring.md#how-it-works).
+This setting applies in the context of pull refreshes invoked via the [projects API](../api/projects.md#start-the-pull-mirroring-process-for-a-project), or when forcing an update by selecting the **Update now** (**{retry}**) button within **Settings > Repository > Mirroring repositories**. This setting has no effect on the automatic 30 minute interval schedule used by Sidekiq for [pull mirroring](../user/project/repository/mirror/pull.md).
To change this limit for a self-managed installation, run the following in the
[GitLab Rails console](operations/rails_console.md#starting-a-rails-console-session):
@@ -511,7 +534,11 @@ Plan.default.actual_limits.update!(pages_file_entries: 100)
### Number of registered runners per scope
-> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12.
+> - [Introduced](https://gitlab.com/gitlab-org/gitlab/-/issues/321368) in GitLab 13.12. Disabled by default.
+> - Enabled on GitLab.com in GitLab 14.3.
+> - Enabled on self-managed in GitLab 14.4.
+> - Feature flag `ci_runner_limits` removed in GitLab 14.4. You can still use `ci_runner_limits_override`
+ to remove limits for a given scope.
The total number of registered runners is limited at the group and project levels. Each time a new runner is registered,
GitLab checks these limits against runners that have been active in the last 3 months.
@@ -749,7 +776,7 @@ than the specified limit, hooks won't be executed.
More information can be found in these docs:
-- [Webhooks push events](../user/project/integrations/webhooks.md#push-events)
+- [Webhooks push events](../user/project/integrations/webhook_events.md#push-events)
- [Project services push hooks limit](../user/project/integrations/overview.md#push-hooks-limit)
### Activities
diff --git a/doc/administration/instance_review.md b/doc/administration/instance_review.md
index b166bb32aa..6289765116 100644
--- a/doc/administration/instance_review.md
+++ b/doc/administration/instance_review.md
@@ -12,7 +12,7 @@ If you run a medium-sized self-managed instance (50+ users) of a free version of
[either Community Edition or unlicensed Enterprise Edition](https://about.gitlab.com/install/ce-or-ee/),
you qualify for a free Instance Review.
-1. Sign in as a user with administrator [permissions](../user/permissions.md).
+1. Sign in as a user with Administrator [role](../user/permissions.md).
1. In the top menu, click your user icon, and select
**Get a free instance review**:
diff --git a/doc/administration/integration/terminal.md b/doc/administration/integration/terminal.md
index 882580b35b..45b94781ad 100644
--- a/doc/administration/integration/terminal.md
+++ b/doc/administration/integration/terminal.md
@@ -6,7 +6,7 @@ info: To determine the technical writer assigned to the Stage/Group associated w
# Web terminals **(FREE)**
-With the introduction of the [Kubernetes integration](../../user/project/clusters/index.md),
+With the introduction of the [Kubernetes integration](../../user/infrastructure/clusters/index.md),
GitLab can store and use credentials for a Kubernetes cluster.
GitLab uses these credentials to provide access to
[web terminals](../../ci/environments/index.md#web-terminals) for environments.
@@ -50,8 +50,8 @@ detail below.
## Enabling and disabling terminal support
NOTE:
-AWS Elastic Load Balancers (ELBs) do not support web sockets.
-If you want web terminals to work, use AWS Application Load Balancers (ALBs).
+AWS Classic Load Balancers (CLBs) do not support web sockets.
+If you want web terminals to work, use AWS Network Load Balancers (NLBs).
Read [AWS Elastic Load Balancing Product Comparison](https://aws.amazon.com/elasticloadbalancing/features/#compare)
for more information.
diff --git a/doc/administration/invalidate_markdown_cache.md b/doc/administration/invalidate_markdown_cache.md
index 7a880c8184..855910fec6 100644
--- a/doc/administration/invalidate_markdown_cache.md
+++ b/doc/administration/invalidate_markdown_cache.md
@@ -15,8 +15,8 @@ in the cached text would still refer to the old URL.
To avoid this problem, the administrator can invalidate the existing cache by
increasing the `local_markdown_version` setting in application settings. This can
-be done by [changing the application settings through
-the API](../api/settings.md#change-application-settings):
+be done by changing the application settings
+[through the API](../api/settings.md#change-application-settings):
```shell
curl --request PUT --header "PRIVATE-TOKEN: " "https://gitlab.example.com/api/v4/application/settings?local_markdown_version="
diff --git a/doc/administration/job_artifacts.md b/doc/administration/job_artifacts.md
index b4c16e007c..46a9ee1167 100644
--- a/doc/administration/job_artifacts.md
+++ b/doc/administration/job_artifacts.md
@@ -446,10 +446,10 @@ List the artifacts for a single project, sorted by artifact size. The output inc
- on-disk location of the artifact
```ruby
-p = Project.find_by_id(:project ID)
+p = Project.find_by_id()
arts = Ci::JobArtifact.where(project: p)
-list = arts.order('sort DESC').limit(50).each do |art|
+list = arts.order(size: :desc).limit(50).each do |art|
puts "Job ID: #{art.job_id} - Size: #{art.size}b - Type: #{art.file_type} - Created: #{art.created_at} - File loc: #{art.file}"
end
```
diff --git a/doc/administration/job_logs.md b/doc/administration/job_logs.md
index 64d9248cb1..f2748305c2 100644
--- a/doc/administration/job_logs.md
+++ b/doc/administration/job_logs.md
@@ -146,9 +146,9 @@ a background job archives the job log. The log is moved to `/var/opt/gitlab/gitl
by default, or to object storage if configured.
In a [scaled-out architecture](reference_architectures/index.md) with Rails and Sidekiq running on more than one
-server, these two locations on the filesystem have to be shared using NFS.
+server, these two locations on the file system have to be shared using NFS.
-To eliminate both filesystem requirements:
+To eliminate both file system requirements:
- [Enable the incremental logging feature](#enable-or-disable-incremental-logging), which uses Redis instead of disk space for temporary caching of job logs.
- Configure [object storage](job_artifacts.md#object-storage-settings) for storing archived job logs.
diff --git a/doc/administration/lfs/index.md b/doc/administration/lfs/index.md
index 682352d8f5..d2f220e379 100644
--- a/doc/administration/lfs/index.md
+++ b/doc/administration/lfs/index.md
@@ -2,18 +2,15 @@
stage: Create
group: Source Code
info: "To determine the technical writer assigned to the Stage/Group associated with this page, see https://about.gitlab.com/handbook/engineering/ux/technical-writing/#assignments"
-type: reference, howto
disqus_identifier: 'https://docs.gitlab.com/ee/workflow/lfs/lfs_administration.html'
---
# GitLab Git Large File Storage (LFS) Administration **(FREE SELF)**
-> - Git LFS is supported in GitLab starting with version 8.2.
-> - Support for object storage, such as AWS S3, was introduced in 10.0.
-> - LFS is enabled in GitLab self-managed instances by default.
-
Documentation about how to use Git LFS are under [Managing large binary files with Git LFS doc](../../topics/git/lfs/index.md).
+LFS is enabled in GitLab self-managed instances by default.
+
## Requirements
- Users need to install [Git LFS client](https://git-lfs.github.com) version 1.0.1 or later.
@@ -346,8 +343,6 @@ git lfs version
## Known limitations
-- Support for removing unreferenced LFS objects was added in 8.14 onward.
-- LFS authentications via SSH was added with GitLab 8.12.
- Only compatible with the Git LFS client versions 1.1.0 and later, or 1.0.2.
- The storage statistics count each LFS object for
every project linking to it.
diff --git a/doc/administration/logs.md b/doc/administration/logs.md
index 990287e390..a9fd698a52 100644
--- a/doc/administration/logs.md
+++ b/doc/administration/logs.md
@@ -1069,6 +1069,14 @@ For Omnibus GitLab installations, Praefect logs are in `/var/log/gitlab/praefect
GitLab also tracks [Prometheus metrics for Praefect](gitaly/#monitor-gitaly-cluster).
+## Backup log
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/63832) in GitLab 14.1.
+
+For Omnibus installations, the backup log is located at `/var/log/gitlab/gitlab-rails/backup_json.log`.
+
+This log is populated when a [GitLab backup is created](../raketasks/backup_restore.md). You can use this log to understand how the backup process performed.
+
## Performance bar stats
> [Introduced](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/48149) in GitLab 13.7.
@@ -1082,7 +1090,7 @@ Performance bar statistics (currently only duration of SQL queries) are recorded
in that file. For example:
```json
-{"severity":"INFO","time":"2020-12-04T09:29:44.592Z","correlation_id":"33680b1490ccd35981b03639c406a697","filename":"app/models/ci/pipeline.rb","method_path":"app/models/ci/pipeline.rb:each_with_object","request_id":"rYHomD0VJS4","duration_ms":26.889,"count":2,"type": "sql"}
+{"severity":"INFO","time":"2020-12-04T09:29:44.592Z","correlation_id":"33680b1490ccd35981b03639c406a697","filename":"app/models/ci/pipeline.rb","method_path":"app/models/ci/pipeline.rb:each_with_object","request_id":"rYHomD0VJS4","duration_ms":26.889,"count":2,"query_type": "active-record"}
```
These statistics are logged on .com only, disabled on self-deployments.
diff --git a/doc/administration/maintenance_mode/index.md b/doc/administration/maintenance_mode/index.md
index 39ee357cc2..d5bcd13266 100644
--- a/doc/administration/maintenance_mode/index.md
+++ b/doc/administration/maintenance_mode/index.md
@@ -75,7 +75,7 @@ An error is displayed when a user tries to perform a write operation that isn't
NOTE:
In some cases, the visual feedback from an action could be misleading, for example when starring a project, the **Star** button changes to show the **Unstar** action, however, this is only the frontend update, and it doesn't take into account the failed status of the POST request. These visual bugs are to be fixed [in follow-up iterations](https://gitlab.com/gitlab-org/gitlab/-/issues/295197).
-### Admin functions
+### Administrator functions
Systems administrators can edit the application settings. This allows
them to disable Maintenance Mode after it's been enabled.
@@ -111,18 +111,18 @@ For most JSON requests, POST, PUT, PATCH, and DELETE are blocked, and the API re
|HTTP request | Allowed routes | Notes |
|:----:|:--------------------------------------:|:----:|
-| POST | `/admin/application_settings/general` | To allow updating application settings in the admin UI |
+| POST | `/admin/application_settings/general` | To allow updating application settings in the administrator UI |
| PUT | `/api/v4/application/settings` | To allow updating application settings with the API |
| POST | `/users/sign_in` | To allow users to log in. |
| POST | `/users/sign_out`| To allow users to log out. |
| POST | `/oauth/token` | To allow users to log in to a Geo secondary for the first time. |
-| POST | `/admin/session`, `/admin/session/destroy` | To allow [Admin mode for GitLab administrators](https://gitlab.com/groups/gitlab-org/-/epics/2158) |
+| POST | `/admin/session`, `/admin/session/destroy` | To allow [Administrator mode for GitLab administrators](https://gitlab.com/groups/gitlab-org/-/epics/2158) |
| POST | Paths ending with `/compare`| Git revision routes. |
| POST | `.git/git-upload-pack` | To allow Git pull/clone. |
| POST | `/api/v4/internal` | [internal API routes](../../development/internal_api.md) |
-| POST | `/admin/sidekiq` | To allow management of background jobs in the admin UI |
-| POST | `/admin/geo` | To allow updating Geo Nodes in the admin UI |
-| POST | `/api/v4/geo_replication`| To allow certain Geo-specific admin UI actions on secondary sites |
+| POST | `/admin/sidekiq` | To allow management of background jobs in the Admin UI |
+| POST | `/admin/geo` | To allow updating Geo Nodes in the administrator UI |
+| POST | `/api/v4/geo_replication`| To allow certain Geo-specific administrator UI actions on secondary sites |
### GraphQL API
diff --git a/doc/administration/monitoring/performance/img/performance_bar_v14_0.png b/doc/administration/monitoring/performance/img/performance_bar_v14_0.png
deleted file mode 100644
index 42261ddd720439ff7356d00688d9208f7bde84c0..0000000000000000000000000000000000000000
GIT binary patch
literal 0
HcmV?d00001
literal 13115
zcmb8VWmsH6vpnaaBy%~@^aD|aBwJ~SK8r@2(*$30E`q?&mALXs4y;p{J}QWZ~?{W@hPZZpG%~=<+HB
z2Pfhq^h!Efd6-f8ID(zrg?vP*|4TyXmHx+Or>6QZ5f2AZYCUB&Dk*0-D=J<#9yShY
zF?1>_DiJqJYatEkkN*vSeG;X%_3&^JVrTdE_Ga_uW^;D4VdoSS6lCY%V&~!lzDfYy
zeVshae1J~wH2+cZf9sL9a<_1^bMdfqcB1;H*Ua46(?gV+`k$fy8UJ}tD<8Z6W68<=
zzukIukp15dJ0}|l`~T>Eg^K*+3aQ!oSb_DV?HsL~++S^oaq@BTi~JYh|F!jhO#TO|
z?QZ2JVVC(Gh-{Akf`Tqp|mrecuV^fg(e_Q-t8~+UyVgKj(|BA$a
z!u(&{*Eoxzi?IJs&cx8+pW)--;1HhFl(b}&(coTQUSO~l$%OT_wYAyVOD``kMa3XS
zCbQh(tKL8y6%4p9UnZBAme>kSwTW?erI>PuE&~F;4z#r{exFmvM_0o|YG_!@>i)sS
zZT~U?KR+|Gy1M!ZrqAv>Zf;(g8XkInd5%tRN=j;fnS^gCE7qnUt;^2TC&4X^jmpg(
zcr2#V1YlRDBy`kNjgBrEQIy0YSG0*88{mn
zK58y55EFB^Z`4r_ptW#mi>?y5|{>CORkygX{fhV)J*em
zewinY&Fk!IZc!>F}k=Dv4O>C>|lX2NaDsr(pB*B{Pv+y;19Mj2SY
zDJ$#b(a&y~zt-HXd-R~YF=Z%n6v#ZE$q^MRb$oBG0@vL$ySaJzs~@P@UfeCg&7x}>
zRQDtIa{vVLmBr3f?9&J^*3`Hq!q+#au71c*hu5lPPo~OUMNIeM=J~va6UpKI~Aa_Vo0$9_+B@yyE9z0s4VMue_S#qTDYrUdKT4uQxhvLBA<
z9>4XiE`QVZZAnGX99kmrX{f*dopR3GHA@8he3q?yPS$1@g15F2Ie-%(vLd|T*J-Kv
zr!ltBLjnD|5hiSW*FqHAo8&T`yj39FOCD@7nfspVCdg{=Oc(Ri>rcQ(H<~K?n~>H0
zqj~t=lqG*dT|7}=W-sZaH`UcLG3F75G1JR5k*Fw`jY#RjC58}l#HXrgB;hBP^|it8
z)8e1Rj6Qvx4(xJt)uo6JwdqVR`zAs|zyqTxbxW9V`qSKnaJCANysZ=)BIr?L-$7Y8
zwwYs5?>K+agKtlD3!)q97L~uDo@pU#NUzx6wC1%p$)neG&`lcI5gAPg^!TJ3=Rl<8
zDJUK*4XqK5RNw*E$ea}}`j4y4E3KwlBy@{4O%FJp{hIi`O}G#^OCRR3P66{@wQ(A<
zz&-aAj4wUok3IV=br~T<7Afp)au~#+rRY+DypEKYy&;Zz><8t{QF+$_Kh^rxMmDF8
z_eQjJP_DOCrZrf;pmM_1_sg)HyeeFsq$~#9%{r<8T(CtSF;VN!J!tgW0d$lx40oxP
zYk_bU?d8N>E(_6w8TAnCk--^PFTR)R4k|C+8}ANeGr
zO-0w-I<3-o8GHJm%A<72m8AW3Q}n08D$=1eENp@O73V`tKPmu6jMWiQkdtNWFd#6T
zd61M>F1sh~;4`HmMcITglE{2)Z!KLxmzq?IjtH${Zpj8QAF1Hsz(%Ef5KA&`l;c%A
z5jlXvy8g@8csw~voKihI{5?SGG@HDxFY9ylV><1nw!Cher$OD^oVYEt@6tu_$INzS
z1DxxUG`**RAJK=-+%PgUTL(?$W!I@ej!NmEDjjF@D#a4EmVIRBJ1pXF+D>hJQe)`6
zsj=Z980|hHhEwy@GwUrCXA+FIrIvE!Oo?&Zq#w6(vV7Xu{N0`ov@T@5(L8kQ6Y?vbfnw_g+BuR+rJZ_6tSQ0w5KTY6H
zej-RnXGNifAn}s=%@f89)w&y!N;@SQBATwT^qJV<<%T;XIK*{)eHRHW6NE759)Lvb
zrPvrF6FTSoE*-j#Ru~7$DdGD9e*kR~_m(_osTG2Iz7X_@`CHd9+$|yG?I|5|5Rq#8
z^RxUK0C5z}?%=*6|4l`0QyWxOQBWUE#0Vd!jN;pLNc+0mo8oU_prV=Z`P_t$+ya4D
zxQE(?to$`a4k6Y025d9*;(o5xZYl&HS4##8SyWm}Q6|r>T!+Hl=UR*OLn^;xjh(!n
zLNnDMk-*=_PwAV6){(mnP!Rppy4kus>pQ&B1m1P?$?lZ7qxVdJlz2?l&lM`6du*xh
zcyTc!OJa7IcXYMK;yIX})c!e0l1pqDeYm^GjedH1-8so017qwPTqb}#iEO%}Oq&G;
zBWacgx%G=RpUK%)TTjQtEG@A+%mk1
za@QLYGEpOUg1yQ=hLRW;bjz&fdALB{oGI>q#Ivr}YlVED($t7n;>#p49&f5bTa)Kr
z*KL!*`udgoPao2;t*EJzBqw;h8`Rkm4f?^vnOGdGq@VNAaZ!T6w|}*2`?+Ftc)Tk_
z&yjJxZ^?_Dd4Q7!xn7^LtzM#q
zw%*Y;fCd>+_%K1-DGn6>f%QY16~JdkBe#_%K$d{nR`N%R97LdI2WsXLfm&>fyIr-9
zG=VqMq0mN$nMY+|aM^5@?G+}P;hi7Af9-v&^^
z2vLt7=I4?k(qZ3A(EL{*2kvPnhB{obTQxuIzs6{&bu08-1yws!;
zh>USK(MFXk?Gq%Z!BdrBYvl@?-Y}rq?@f&`NbR!1K+-6et5%|tz!=5CXvUfZ;}EN;
z-noA5E8b?dk7ny#PHl<*J@>S%tOsv3%T^;w$0F*amlf=jAK6;cnqcwGfVVjIcWHN;4qm1(~Vx?=jhS!gyAA|G%Q4$Wq%SW#W#qW%+XwFE|3dK2n2G%9>p+wRIU5x
z`v(Z9r`UrdQWfP}n-%e0=bpuOSW6}=U-k~#>CLjdDAew2RG_0jLI@1{BDW}^JBz#NF=pu#}$TvCijNa07Ou9-c
z?WM5Hq&VZL#!>-gwdcD4I;k_%3-(lbJFrl#%^^mF1gmq5&2CGb)R%-(Y;Pw|El2QP
zcw9exX~;gX#>+}OcgQ|9+-KKhGk$8)I)B*gb6q-M;qEwoH)q?**I#ZM`9q1Qn3r(ZwVOX1^&{nDrZpijQ)Vh}nk3q3Xp{D0rIn)jEW<
zdJyU^d`zv679k>
z9zvd*(}#gU)4G_q1l*k1NAV6g>H^9V^&1goyr2Ot!@hI6j{)8GlZ{O_!D&~EnIUP%
zLr}V3Bq1jgB;FejR2r_6JT^OZtbJnAw`KV|sfj?2)TO`QtOrOvHu!fajV_lSc4lv6
ze|>8}#_qBB;cpIgRioB4)u>YGLLMzBMu8ITTC`DpK7Ys&kO>_wTa
z;jH&!^A&_Io2M=A2&BtZS0#TWO=Ct;^(xPxqI5$)=b+$gqQFiFm&fw57}pd5kK^SH
z1t{C4^ocbLVio^$0-KSV)9tLriFvbnRq@_kR3j2#i!n9Jw57U5?H6lw$(&JA
zM99ZlKxgWlvz_XFtit5D8mT`f&(AE5O3OtL>He&rPBXzu&?)4fY(F=Pp)rphFma1N*|QKr9@E-fC*z3#HTMXQl{FYojN6QK#MlYN@(H7-QpuIv-Z}9%oA%hiYH(K*d5azkk>+NsOcG)6kV3%fSxYtfUjPr7>Rk276dFGz9_or
z3ZH
zCl^}`DWfOf%pgfuPwJykB3g5Vt0uy_<>s3=BWF@%wS)~;g!*JazYzdU09Set>UgzD
zTtiAqN*gQ0pJ3g^$fo#VstzJH$m3kzTUSNUcxd**XQwou=*R-2eWOHmpD;*nR9fg^
zGJ7!ueSOC@N3-Iv!T0a!k6lsdKr^3)%)-l?
zN)$qb?#CzGJH$)^%DQcWd}nfjC94`#bwF^Y=?agbrGYE#W>F%O`YM74f+#f)R{txj
z@+kaa4FZkgHmddU?MEHc#{=zJ+Km*-1VXSeo!77Qk8aJr82azk6=7z7*peMH@D~-U
zT;YmGj84MwP?3NJd@?$SXg0z%g`6WTW)RhF8_;LOO*?vI@!v-j;c8vF^LRE>Ao!Qj
zfXb$q%!%I+WJc!D{jSma`MZIHglDzR>ZIetPR9@^8HH71M3m(X9O}w$VkaLom9j-v5p04FK_&6jFw8o#yB#up85em3{^^>T
z%A@V77l~HJCPvsz8OQUpvEfS>4Gd1Tk**gi##bvU(LjyGU=_NX&+#1adC9^Og7=3&
z_;m3D%N0`dkr{g8;?^M3A(>y+HjS3UFU>i
zmapSiZu)im_{xH~Wgc?|2q!msWeR0fIvB!T0|WnngBL2_z)G~r&}=t`CxtUU#e|1&
za#rPmP%(&B@XZM*Se5B6D@?~RZY$r==;%pq1xJjBg?z1q*Iu7YXt5nhVD_yGs8FS_
zrhx{a2RhTY(mgxE;KTvGH>QT`A!1`Kb;yqrM(jeBQ=X8mYqPz#FTvcS&U6
zuV8r}F9;!5E5nzyN(#qwmW~!x%Hk#E_Ty_|5JvPXgY#=ZA4{@z|25IYjnxYrGf4
zg!L_lNKs*c5X3i+-x1==u4&>_m;W;^E>N3Ag8kjN&>K2fV?~miTFKqHC;|ypb+*|p
zA$#%@A1LPy@jK*e%RdF&|(PPDKy`N#jZlFIhJrkL+DTi4t4duXNaV}s(_1uHL(L%i*+
zk5!Y*BEpg@Hv3ZS{%!V3G7C5GdsO}d$vyddq6IY_WQSz2ukOU(K_
zcsu}@=H(aZAGKft6b%N`{8Yd6Bs%W{e0^N06DyAaeq&6!n{v9;QRpX;>F==O#TkUs
z7=&c}{4teiYNOYl&W-TYlL!Fr#j*6+K`WWBnwGIOXl5P=M#&8#K-&J0ermMSCEB!Min-W+G@N8b~#_Qs}9Z9clJflyn69*Y}q9JV})I&tryB
zM!|WpWhjpn%UnlUTF&dLIO}}`W8Hl9$W?7_Y}BDM$Zt=au0Yl@4u@m!tgi~u6;ji!
z23awJot|#)DUAMHm^>aL@FmrV2ObXrwp6EA7%lp%e2^92mJ^U!7Z!rZ9P?RpeAoR^
z9XP5#*z4jX-9CiL@Dtd-2!P7;hDX>{L;-hkNQU(DGhDp4PK+mzAyvJj%bV`g+)rSF
zCGwuYA5N~;s4rB}DLZtoMo~6`0=if2rC(r
zX*4R{!{n4DSE-((lA|cG2qUEb;K;nyqUx)i>nlUIlL~j*pgc42zu34B=*rp4M`H^R
z2VNZnh;@7aIS<_Y+vT5bH1{VuIz>BA%9#=L;gI=DTC*LY>mCr#N#g82?L-P~ZE_kT
z+5_W-E-oTR1QsnBe_-G4r4FKNQ)Qh4t{tsDzzl`_lrTNro58&rHov}awnPA0#&uj2
z4eCL1B$BckAO^$zVUQXptDZ^fB+k8z*e_(qFzyVK4=O_th*jT(FLq@1jJQ{-6{R>2
zU-fd$+t`-G{?FLc&vWvtnvCQ8
z-mMs^%9U$59ba?`!pR3VT^ifqZ92hKKNQIQm0p*0V>*C>Zl)S^vqyusH_XdfH&hSj
z-`JNWg6=d1zlBeUw)cKU$Lh-I^VAY#R;nJ+qqB;{^01A-ASq-m_IBHe(Vy@^;MEY?
zXo&Z(mR>8oF2Xn!D-aUAOy?=6^qa^uWc+fw3+rxp;S*)jtA!o5o}GZFGk9&Xz8-
zRLz$#KacC?XSihm#DtoS5o?JGa>s|@?R)6>L6})W*nSX?BRNsqHeZnT;o9ag@VA1#
zSeHy^R~ug$$kO2|SHOVF#EQk2Z#brtYZJ)s%2|(imrD)HO#d>uNi+#O#S8?B3wMNV
zYC-TJYZM2L-`sHaW@i$D@OJOFM5ozbc%h(GwfePaXL5Bm34P&pRu@QYtkm}ALsqzl
z%A-l*ACwx+z9qJNX}6kc4EvuSpC6x~ouaI|_9&eE3dWzE+x%JPfIB7ReTPQ}GgF_!
zu@xONZx~Yq^sDJmvYv5O*9p>i(OSSn-wu>BJsSaAD5XD8++m9C;V;SfBK}{}HosaY
zl!65nV1;4{-jMe^o7a4+U2Pr_SI`Zgh}7Exr6Z_rpA%Em4}d=TY;R0+5~Og>l&+rL
z@e5ZLC`$Uz?kEilU$PYLbDm0034#+`x8|iKwR-*A_Ha?mxTDMYa|TUs*I_SSp~|)k
z{FbU1Y8{VGtxl*?~zcK3B!6T!tp8Icug@6GU*2a4;m)D|Sa4v8fGFbq-QF!<6+r
zga68ZUy{i}tuuXzdQP1z!<9p+s}F&FYo9r<>Jv^JRA;@WyQjrCRc-e4Ye421VyqkM
z1fA@vmbkE*kh4V`|D@AslWjFg0`>hRPV`y*;x_$R=-q$_arJV{t)&%?sx75;K#n~#
zz99US$o(9tJ+$f|8==!fRc&^<
z-)*AW**gX*LqMj)4A4X^dxFmsn$0^oacc=wpS3mFEyu~z{UNy`&wE&az!$hyI{E9d
zv}x-hcsRHoV4~~mAHh_=nQt?Zr$qntZTx>9BJhO&gcFT>fdAIkf88N)E&rx%z`yyG
z?jiWn&44lT#_yq!Cj>prbYu9c;8F_?zJ3B7A`y-B
z3i}SHb(o2qD(8X$_ZbBDT8;f@>G}8g=$3p>e%7_Baxknz<5Pt{&8`=b;MbC5b-i}5
zxL~x>6%(83eZ+q$uU$H`wa?H};^Prq@9ffd9Y>y&x~gEw!cdF+_nzy?vz-8X=_1ht
z$&~dAhi(KuXbpGj!gp%_L`=
zDGbxZcrd(hsdI&Ipa>pPS0v2Z5Fw=3SZ+g=h92r?=Q7TH0Z8ZWM>kDTIk+nKP_|7%OHuovwE48*@>y~j
zHS})qMASd90hAY@rv#v*EyF_{ESq8eTA(NOx6`86;g8qeffXOwgI!~Dqw@-Qyu^#b
ziuW~MpI1B-3pEGLt{B|2o&czszf8_%t+cM(e@gMqGHf_=3fAB>I=x@+EIvfo+hTfG
zR-#FF28jFkj*yJHvdno!w72wtxM7?trZs$bT~a+6Ke~YwE{_mP#VErvj~Jo(KED|2W)OoR09Zm^`7G_$)N|s;6{khmz3E
z#9-q(BWk};fR-MQ@NZTTy%W3U9c--}Zy$Pl>+x-{nFqfdHTb479BmY@o(_cL|c!Q#pOf+c|w~ZHqy)si^CPdYp>M#zg~dg
z9*s}V1{-eSQI-nt@Utlh-PKd$H0|#~Vt1j>R^dAfDXt2A15qv?l;4z)*;taemmLA}
za%Mk0pv5!Kk0x!urk#Di`AslHq-eXVpW
zdHxYf(GqtZACDShGc4Qb^^Kn+h|ji!am-e;)qn2if6~3H6V2D5px$TFb_E!-rm-4o8>;%DUtgsp{k&wok0TdcYn7OFCni_#CMYJQSEZ)vN3=iz%Xv&V
zX%3JI3^m|w^SMR758KQSbp8RdSc^aJ<
zzc}J0jobXNed?mSRK6saUiUy1=(%0-v6BwGy8WRKR$xBC*(k4lTbY
z^FJxxkbEL)4zv$D
zJ#YH?gzY5YKcH@SdPHz~@RedHeSG*1JC@zGrJN9p!YP$bE|B-dzh;Z;<@{w$IDd(6
ziYNXsu)n~J1X{x$ifxA59*2$c6F1i*4et#n(GxsP7yvWkTt<;-Sj`9lKo9POWOAf|CwXD9e42=La$1)3
ze&O%zuf0G9xHo+dsaaYS3OUkU_e(FEpO|%@g`_L58p8|e{-&9Z7oQ}22GU*~&(cI^
zpP2mSPDH8r;K7Q_JF-|esjJP6rT-2^ZDkE{0K7rXl+M7sp8F!H
zvF|sP3NBa;>smQ4&y|lonJ9g{B`oa;@z(4|YAE6mG{midl2Kc(mE5woa5j}RV4dXE
zeZ=l9C#EqaB2WfPape0MClo8iR1L=%?_1#rb=da^vvB))R()+!D~^KZ-?`+WY-w{s
z%8{4`dM!!@E)R;LxmjGXp4J0$2BpSeGw2-3-dMbRrK!qEj`Lc((;5X!N)=D3%Gp2^
zcvJw)lsupl7QnuQKrgEm3Cu^(-ArzA^NiBja}_3xv-wr)zj7VxdNA4e29LA;got>g
zJwOcyFC^6w#i9`FcIfo)W
zFj(R*lo8d^g1=Q`tj&Hl-D5F{OlZQ2o}Sf{g>
z71r-tm@)0_EATyL2CxGqIFrD^XI%vu3!^QVri$5#U%r3;F4buDf$fv{7oLT(WDBsq+HIr$kJp$Qtrj#A4==UGEl{G_`p`zLr7(IZaE9I7k(e2Sc9hMLu
z*s|-aifOel{g6>{Q#oP=kt_Fi!K}to3#+2iS&?Z?pg?c`d-r>Nx?eadDHrldqVywn
z+W`<$$V5wr3P<_@@BL7=GuooP5dF$d2g-e1I&g_#yJ3E`vFjdim}O0C@W@$T;7r?i
zJk&!)5A$zoM?0a#U%CGAA0OAF+g$W-!v+Kl%)JB1#otpRn~#gU$2c@M2f>@02M#V5
z9mjknX0}k&7i9b_S*Bsd9tD=b8iYbl2KwNGzT}?V?ebPMZmJ$~N#PF8{bB?0*xw%y
zphu+hxDF-QeyM?pgY2m>`47w#hsUHLoOCAf=~SalJO)^9#~17Bf^_>^7csPWttv_l
zgVh8y&RYy4{-1L71h#Aa?YKmE5$W^qm#<0W18%FUB)aNUCIVIMGW#wLd~gD276{^@
zeg})yAX)!!vWesD*Y6*`KMU0tEG=XM^IeANZd(ZmLlc)Rx?Cr9y8>UB?2ybALA2HZ
z;OK<+>D{$ZLPm%wc$5kCG=^x-EFDO1TKX9*VH6Lv7ZAD>1LQe5ijI9XC7X{IQqN9Q
z8TTsRZ=gG-Uwr~IT`n)a`%4}a59A=!yyyie56mr!f`95|-odlQ&(Xx?YF+%g0)7e_
z?ic@pINahY_D3&`ZNsMLp=k60~S6toVtz@XU9)=&{jZEZv4n
zVbR7=j8b@%Aps3ClOfipyX~bJD;QRvvgUTWZ(U0KK55Qp_@I)~S+12;_&sOW&YnA}
zB*nqK^6wv!6tDp45wKyMj26Rm1+Qe@u+((RJJ>||6^vNUR}l7&LzONC07m;Bh?1O%
z4Jvp#%^m@0alm37rikK>n@j9wEJqe?-;Y6Ae<;|hFX9P-gAFCB6|gEHK)PJ}W(45!
z23TIB-!j6lKKw#%%tF~f&aY~IndMyV_F*+lh_m#Yw_=B7Riy^&8_=EV8ym6uEtE{v
z*y(%fN-=BGd)4%O)v9l2Po)k;W5Kpb?p<=Q3As7UYI~_ZnTh)bUuW<7KEUWr#vwt9
zYrPZ+;a@{q(4wER4abSf*lB}|W9!~^>HOQS=m|a~khH{Iq{OC4x8d`>vrl>>0NboP
znzZU_O4}|wAOme5ZkOYMcABw1B@!4h4j@Z@x^Fwr&>Laq_-y-CjUUvita*~=Jj=MS
z5mVPDayi`GFp!APAB!KV1Md%K({z7g2QzbEY}v5NK6Z&xDlJm3%us3U$B+R
zQJi2X+jQm!kn!()1Zqqi*ozwla8<(0ULal;4zBHS1ZTxHn1(~b!(D;=XYnyDmK@Ah
zp325{zvDGl7Vq1X2X2jr5i2yhxOZJ^-)1NHC1IrGESbchnn9N2>mpv*LDVdV5k;x<
z+P41Bg12ev7X|=PeIwC|^~xyBpb$}=c;;^}
|GZ55Tl1b*sb8R|6X%Q-+hB`IiAr$`9PE3
z!>X~q7kbU|B0r~CP$~|ZZ;b0dIFO2sEL7{p1*wfd^A&e&Veqqci+q@K`Oh?Q#>X(d
zC6v=ocl|gxS5mTtiNFzPwC>l{94BydypF1|4)?;`K`fRzx{05l-~hZ+G89v{o#*9mtkw8sJQS3)F}Lv-rIyA=$2
z0nuQILV8KT)0x_Mw`GbXAme)Hc<{kBI1KLY4#913hYW7P9fJGd!7TxT4({#_gG-R$4hecW=iK`P
z-nHKTFnd?+-PP4SPgQrX-A|;diYz(`5egg}9J;)mlsX(78uax(1quH37hbhy?Dg`_
zT3lHi4z4C1_0bgJRes~DE-L|7H9>L!2lsYcRY_A?83iseF%b$~dU<)t8N8UDK3`s5
zW?(Q?RPIW!*5G6sA3{M+SATWPaAzC(!@ivv4QB5;5-)6j`jDI
z#C&y=6w)Fiau(%ltN89ABih!s{xSsj@bsubfRpX-o!x(GY#bSGq-)Of(byzGRMf39
zJ+-s8+TG^M5)78?^jV*t4$RAHAuc^IuwVNv%aD~dC#R2MDP89yMTnk8N=o(1D4eUg
zOLH+Y
zU=1T11$9S3&a$%czUIckj&>d%a6s6%&T_ZO-#NjPWMo>Cj(cTHJ9WH1(AIL>f!W?M4TEj>w>0bisu3*xyj?=k-`dQ*n(tzaG5!OK
zQP7md*UX1Y)+~1;!2mcue)7APc`QxnvYpm-zvK33c`t>j#+{U*EqO)^m^$@Swc6(!
zzi`-4!7?Zz^OCY5E}MiOFLTxYTuYt;@55w<|OY#V
z6=8=v>U$ed(*{g6F$!jsi|z
zN?h~H@^QBD=anI%0e7)?dlFxlLo0I*sXnmRmw#3Zfv-BDrb&1K=CgA<$YnxSsXsUa
zwjmIwq}m8td>Z@Yt{xA)We3yz>nh&;Z83~id>@+!>xLpe<%)FulTIi=0y0nvH+Z-S
z3d!8?2vLmg5C3VU{#QHGx-~q!CH~=DG9h&qV*vaKe9v
zvj{#`
zd_QayE+=|)vA&FtCx1zAyDHZD%ww*AsN2~9Is0HU5c_E~pfK^_B&T7%ZZPr_3FNS?
z^Z6M9A2iEuY4}T(%)r?r)Oa>xC-+Uy*7e2O!pUs5Mjc7}EeeL_5QIlU!1)sCJqPeY
zF~DM!_jM5JMG(-Py@UO6=gnKiqHk0WmuDK-7lNDi0oOR#g89_~aSjXgmGAeFEr(sNm^bgp(ig
zTovqeEV4&m1l1`=P8ZGu5;R6db928kPzL0Owf#LK^8v7`b$*1yQ&?Qx#
zhjUVZqC#=GPxxPc_xwFD=Zb7o@i%#4LLp9^4NX~5fO28Ays;s!?c1F1SGtncE40pj
z7V){vHDm)h{c39r8zzd3(1Y!w8tq3zMgSQDq-shBop=tBM2)mc$QLQbSiG~bnK`K@
z%Ad$9YA<7$ZKCiyTkNe)@9{3~Q{OXLu-+WFZk+Eow9Kq08Qo=FM=n_MwHV3l_B(L}bwi(?aAm9;FTbJ}@fm!H
z>uTm_1ubq0P@GB|g9>vWH(g|7J2;;)?58wANTyNKriLI@DEP02`iwGE%FoVz+$Bk6
zNdkVLDlM{Mv9N7@`eQ;Y)zJGD9B$B5pU^@!%Twfzdt7=-$f(mQ#1mfVEo7%!HSPmm=l4BbAPAfz~Ym-w(Qu
z&rkL5s0^i0;<}0&}FZrsfqD
zc!w9yL|~O^1ivr-#@bx--rSzyYH2xmS~Emq=+ntZQ0708k_K*8Wl2k;O8ZS=fWf1O
z+ibu<3ES0!8l0S+v9hdF(2<~D(t=K>2O|v&-^`n*+vb21$qp%ltw$Jc%{K%z%RQmC$)K$?CCDjF#^)-_hkxxXP(UZIizR9KePY?{_h=bL;i~i_%
zx6`bO{rL`(A6K^>w^PN-ja4=)9%~{a09k7=7c1x(vZyNk16JwDMS1>c
z1kd3|>9Y5U;!DoF>*>l4(|aS!QGbzKa=vBDlNCqNrCKAd1VL%2+(ntaswV{8tRNRp
zjyK0#ssKeo{`5mnG-^9EV#PzZen}-%3wqKvSf%uJJAQEe@LycKj+A|>A$F*9CP*l1
z#2G4c_Ym87PT!T#n*GeBN^zuoM}?%p0~BE
zPXhuwhPj1%c)Rcow>0GbT$MY}$h-Z|(s7bSDgVgF_^n=lf
z&v?VqP|bC6f6GMR8yXKb(#lmLMj5Nzp%2BB?;TZ8`><5LmSzC;FK^F@u
z{pcR9$8Mp`CtW-5&~YxsSa>O8c5ZnwmU32HiCHyd{K?7unmS*+=bF7IJ{V=ldcf>%
z=gKmW*LfEDF=7oeE4%h%nE_Hd-tFASi1boECi~WjRUviDqui#rZErJX|w7A5LaY~+6XqCe%-jTz5%6m@Qs7$*8ygLD=&_9Jtb?UlOP98^Re`2MIST-
z^-q$%UnXW~YWZ(Fix*G$NgL+9!~kSuvupb=8&{(SEcB)Lm(PY7OHs_WoL)9OW?nZ-g!pft5SNTCqFGf$*M0ZPlG$k{V_yoe)*o;k{*6r7%Tky?UybN(JAkVM5kylL|JUbM544R{
zpxpeM76fzfRRTpYy+k}82;aWr{ZgY-Y;GCdj&02u#vE`yI_o|JA=L7@6SOP>-}77Y
z=U8>qcTXrC1jQL8{t?f%JYma#HR9~hHGjL(81;Jc3+qT}$7Xz^ji5-Yo8%L##AbSG
zSLAp!y6SjyLfY}gs5S+MYUgo5pfTRm{Uoy0pQw&x{pgwFIQ^mH%i25Z_g@?rge@N`
zA{Ko)N0M&{D4&jT`64oGVm9(nKabznEyuM3sB9${4-e7g-N=Zn_q3X0$%%fJ30*sQ
zy`qyBuW_tI<`s~nXTcP%gsGOvzLFP)DOzV6E47Mq&uU5TO
z#vWx-YZuK|AQhR}j}N+lkI9hhvc*k~pvf3}2iV3=I_~PKerQSv^Ga_FvwSJ7ywYm?
zeh>8+BpnL5DOhWC)LPTnKk}R?Z&6@C-wFG;t|N?6edhF*X=MHcH0~DJc0j6aV?WFq
z2pstg!pvxvB+kSKM2@7h$J%o!BK!f{-8A2xXe|T-_u9MOD*d$eNK3o8Ji?G&3<5$D
z;F22#o!w`<%^#nctLJR5bc@b280)?dK04;xi}8;={OLExagQ?bsC$DnS@Jdmldo60D(j6r
z9+W9QkIB`UgY72oEq!OYR;@5~9sd;mUf3pHZm;w1x(h1QpgpQyj8N7u3s)F>U{exzK=dw*g({fUR{iU9y+FT0DY{v
z=Df>0c
zoHt*2+oV7XZL&l3C&}IPKZwaz9vx_s>`&fGVO0ejBU>
z1PGzNT&AP&elDorAv?{_1~zceGTW0Z>-_ZJQux!s?}+go#uwTVrow>S+S5kVAuHd}
z;(E*GtEErpm`NNPS>AZc^gtA^^SbkGPnr`jQe4#OhGPmyHR+_kQ8
zoG+N*>xevXKnky^fF!bTf!qa18Rw6zX2^z5fr>VIvhLBlZ5#;)(RXSKU(t+koINl5x8+SBp>XtDF|01
zNOEm6Z1fP-NM0)2Xduto-Z;S=%;-abidnv0uo#bvtVjLv62w6vld$QH4F*Itl;FCw|T{5m2zdvUR
zIdlx~x8fj0Dbd5lRt+q}6tfsEXoBaWwa*Q$or
zq}dm--{h0yA$hKDx^D#8oBd+E{J-a3UiS~`BE16Du?y1I?NLY0ykI)&vx`59gBG{6=BP}j{N98L=(|`rUAW^ss4voF^jEY9
zYqvRd-Uv69Nqf5#+s+Kqr+Og?w;{$kks~ZM+KXt2!;+d{1V=t&HF!6T^);yxVA~5t
zlW1NQ{(|Sl>Q4bqmpJ^Tj=#~l;I>`PsYR)@Om0fk5D{MnyjX4na(wblE^oH~OoiA6
zr|K2IqxBy6v_9wQq{jtz4tH4X<3-=r_DKMzNf^9})feczojDolQM!AaxC6LIG$aUrJ^rj?!gQi@sZF^RtG_o*9
zXzY3NwyAlIIMrUMMEK_(^4HJlXL5RWBVNqpUyY5#j{Ec?^{Xv)P(rG^)%+ete+_Mg
zv}GJSxwOgNG^sY>LSau9*3N6458>j71Fj|bgXC>HIT2_eleFcn;XkC@!>;j3d#Ilt
z;@2V0(4}AwCJfXtrpc)Yj4>~6^z!M#C@7vrieu$i
zFzkk?397JFZA&K-nb=U^7rN^6Zja(RV%<^87-)tnOXs6&k4SI%+!4OvO_@E7+dAv)k_Io5aswnTgN%4tZRIhyr%~X!ADrTD=g;r-JRR?2O{Q}
z=XI1%rt6I(siw_}LE?cde^;?z^EjQlU&(1}9{|YJ(sMh0#{1F%l@I
zJ<+`51(ft=B&NhaF?
z?S(c(M-{(X_JoD62bVVu`1iEXLV{MS+LE_F2F<%@C)sv`mb~}^tSF|D4Vis5qKwHX
z$}H?0Y7X3QI)0GC_Ps65e>qwu_R(Wub5|Z}tiUrx;J|r7$bBigGVfOt7;Tm4{X7FJtT}snj0^k620+d`xQa(4sPI
z@ysqnx$0BG=^zuxOeD8y^lUu#R~PMVRD@ulq9hgLM>t3A(ENAjWY@dx_)=e(fP~Z6_Y^`bss@YpOYa9w=a67Mp5KWT
z3bV^dONiNtf0Ih}LYS`e_NLsm2d-#dSSG92;s6G^PK-ZCAsPY|f?A(~AN89MYyq&c
z;#U9<{s-XsZpg-h=8=F&A$KY7o@XAWHbGf^9>7i~iD>TBj**5tMh!Yfcu94oztLO5Y`$Zxz4lJqI^_Hc*uRv
zy6bcS{?2QHo351mp)!ZxRI7h?1my$brs%v_)2Ds(1CNql1C)_rG(n#km_=
z+7+S{Gk#jQ$?k3c0u0+b>F`5Di)j9DYLSnuuG)6KDhkC
zB8?V%{^{cgvzitZPP=pXf)m@(XepxYo`adqS44iGMU18pHq;;=kvqcQgCsraxIMKayHE$@E^7PKQrXcG
zJDXOYmu2KZ7x1|U@;T#_80IqDApk2_*~c}FjsY~8C(ZjRK_T4?M8&PkA>Io;mC2^4
zuo8v~YIN8(LV1n0;dHiJf=oekb8`_>QADp{v@<~~fp&_zUV@8IOZ-iI1;5kbd(WMF
zuNnK5WjXKr3`7Ii_6K!tWlih@-wk%*P5d6fkdsib1dvVg9SG8{TRkXOLg8Q~ZWmd1
zjd2=dyKH<+UVeMeW6mwq&3g}H3l*QrZL}>S6cMPFX5`+3%Rms=Lg%73_P+iZpqqcY
z|5D@W&uYJ`2RA8+>Dv@aO&k-+0n0NHd}=_aNKvv@y-yV}|Ka&=W8)?EiQ^VSv?eZFo!672EGPa}u5YMivI~;1_7GCkrF-^iilxSMWA_vO
ze67PgL#rs{%UhYK(iKg9kFhNbe;+d0G4-1M;s+XNbq93AgfBR@AXC`vhSVmj&*OOd
zE8WwH)NRL4uSPs4yyM)Dm6nO6Z^qt{i}o!Og38&I1ir8aGGMZ?TIB_ZB%1*viUp7I+m<%Wqm9lDsO@R`Ex-UfxJ
z)jd$k#Tlo5qh-2Z!9Y!W;hQe)6d0_K909%}#IdOlkZ
zxWW6H0|w%kOnV!%VJhtwiDr=UIA8Z600d3OAmGI6On2ihWk
zu~&v22?f;CfV+j&lPR%03Cv_DNX?(aJ+5||OnG}e@7Tck`p^W0b0xtK%>XyDLT2V}
z>5K&736_}FcO5_Qtj;5(NkM-YP?~1XzS5mD&iq+yL{IXF9aycX*0i&z|>dP*J-nM5fs@zIpb`<=OwL5WIwIj
zKfCmEd!{_7BG)I|7|7Bde{P?2{!yKIB*w^|+#a|I-Ci+X`k=0?VU5upy
zJ|Fg&HPqE+L~FFPhgXWj2<7V_{tW`?&FlPmPctyvQElm;+we6QxJ-qWT+xR&ZWkc=8J
zHTwz)%3yk#1B^Zk`Hf^4V$m`uR?1X0u0@zPTB
z7O=)?*5V;@0-^bOpYIq<@-H^fRFQ&ylwP>R8%RhP5ZG?QJJAE_nh~yQJZ$OFOqOy}
z#C&d|^Yu(9$w7)jmvS#D#pCWpk4Jzlt0fIhP43AYtU2^HoOE2(gvbx~#uM!;kF2JA
z6@==B)ZXENgNC(J`Y$Kh3A|H^ydmhPKldrCoLLdkJ3MsBoR9qJpha2;nc6zWYOI0T
z7GmFo?eCM;@3>E`?vCuCJU$a@>a<+LO=aUOuKfT-PVtiFX#L}XO_*6nR2)zm4|J-C
zZHErBnrbmf8cXf26p%d6(78Pv>$Y3}pbh*4cJ^%)qC1&l!i;QZ+E*K8_R?(Cgq|zgqkn-c;+6OB@&bf%zp_`>;SHvn!UCDezjYA@+P@
zVK%A1KW+s9+1uZaCk1Vcu0DkN=V$Ul2^O1a2HOKClCd(PWU|nZK@cnk5ZC{m1<(W|
z>qCAuExMYb`oQCc2CICLwnT)!!^Y4pt}ThcdFTPSCiH<_=}BA*yv-EVA57G92pSD*
ziX%x<)@z4Mw4D!B=(BqAC=xfHiA=hre_aFm?U2P{?{l{PL?n1Vmxmz-0={%EUO=C=EAD6v|
z5EY{ss~S)(5Q@*ELNs)L!rI!#O++rr)92V~{Iu75oP$$Cv&x1)QzG{Bysp_4mAdTC
zK=}81`a;bvztBPT3uljEHBF!Q=*s)M`Kye)b)#1IP8`s2vq*N)Le{F3<8cYwyH+kA
zwgpk7Z)b5Ob^{L`duysMDy8k&v)+ZtVY*Uqbk*1;RIDA1fc0Z
zw_|}69xdPEZ}(b3{SfPPPd|;*clv#u`sJJ7p3V@)j;$#@dP)Ufv(jhtwf1|>%4|t<
ze#-c|;rck>z$=~r^fW+kmb2PF)KI>kL()BO3};b6;H_zM%kJ=%Lj#POC!Z6u=yH6k
z*Evue?3}f(Uoiy{%;1m{2DQa)P^sm`hay79?8*h%vPB7DJ@)tV*Mmr4-22&i{VX>O^}hG7{)CW9i310RHxC
z8x=Grj$JpN?=%*97ztF})$nDrEypom>w+l$@xkFQOOmnK-Te^%;4!SKcR4
zm#1xaP2_jgq2k8JztLoSbC&1_0ho*g7GY?I^j3rCwd{}}AmACu!kmEVeIpiWUb(1nQ
zQ}7jEvsWyz?c>+X)EE0S-D`Gpk_CIqON!p=u)LUc=l841lKE{`*Uos#Vv5i=uHN^x
zueoUu!qTq)3C{D~@Rc3ke~`NKvfc;vK`33s0+@22+2=i%QA_M#ozdNPu|Qf+FncFx
z^MR(4k!X}xs?l#z07?+uhYI=-GkzP_Pgm3LCCy@(tFiTt63j5f8%?1(HQHiaXDtm?f3hh(zVnMXyTxJKp==aM*Y*S|sb=ybg8%mPLev`OWGr?4R
z-wPPBqt!#W;e1dAJFiLwj3I3-kRICLfheHrKqMUeRm(x&4c!fDQog-N;Ksty4Q0uA
zig`cEo^x;u4d1Z%o4Dv8VP=imU|0(0a*vfn(y9@wUi{`oAruh#{AZrPF*csOl1wRt>@rnZMOLq#E9v8@dHb&`r(GubEfxYBmubol
zQW2-!KeKKbg3!KxVzTjx@ZYJkm`(?~FqfTM#&sCSkYcjo>pX|z>ei!{tIOJ$?#%z;
zW;8$Gv+j`4RxEJOv$QEkP)g)kxFd^2$DR}vBfT94WXGyO9iP~HT_)jXOfYfOF$*}(
zX}pFSRLj%pM0+!0;%gkRzHBlYSYm{9{)_8eX$F2j+X-sjeaL(huEhE31;2
znWJ>b4bRgS;=J%?Eb!L+(!SeK?s>dASyPq`hQTX`4EB<6)MYwOO5
zAU&+e)Fr%3f>MLV5NXFx2Fpy?RA=Tm{Pb2f#p>ITQQjus^skXQFtcTvQ8ubs*SFFP
zuD=2T4NFZGSH6P=A<-Vw9k}2(f=?Sofi^j7fL)*S$5O8AWh
z9X);zx!gCehh-wDN|6)Z+3J3F!$lnejDgDCqOiHXwCOS{$&eC~saA@TT*U)*mJVD2
zBnk)^xRbYiMX#$TsQ{&~xIsHHRnY8};O1pshv(e(ANIB%SAJbdy!B#78(uXLkWnBS
zljd!p3|5a-kSa6FQJukND6YZUsF=)eFk@(pD(IN01OSeUueW-Fui@*ROw-m_ojM0Q
zVDV1-LXVSU{G7>3N)^t6FT9ll*_^HuF9)6zFZrDq)~OXi<=}^F-4&ujf*w3*UeTC@
zKIFcnY4pCEhx7Y#H%_0L4Whm;yaLPTqN8wHTRwx9yvu5YUNN_YuvNiT^n7*NFrQFGTZTb^%RPjCpXA*i4wlKaLoq%^edRA0i<7k!gx6kQG(!%0fY
z#xN+?tpRGZ{d*Y|a+l>R$=~3Uy$~M*uVQ>8zZWmZRolNO)zU%ct>+-Hccb_{hrtHq
z%}6$VM!l8!IO7#c-2suTMRr+35|p>SqL{@bKZ4aN=N1m!_H=!ZE%Cu(
zC(T(ADq$)%5wXRC(`p*z;qc$-5h93T
zK&7&yFd$Fslw|SS8wROr=^bQSzhakIP!pkv;cDZ7^>7DHT(u=k$QtpFq)Og4s%{sV
z1l|qT_}+4X=p7y(%#3&+vE{6+%bZeiMVX3F?RL0EsQl>Y%JSGxe494K%+xHeLq#zChZqsJ0(h`se7Pj<2u;l!+
zD5B8x{?M49Eq_P4s8sTSElh=U<(t|Ra0l(j_xOIl$a``B9$QcgIs$n+$p(d#L@J&i
zRa6%vm#jnpMH*=Uh(uGrdJt{zGV
zCYg|WTwzj^gj5e??YBrjkOmbyZ&@9
zWU=lNExX?kvhf~tKCfNo`!VEM?RVlY1^!-Nxn+kLljYR>o2*vxD)wCB=5eM8o*-BJ
zg~B(i(&$zd*&!G{Sv3|suwZfpa>o9z*M$!5gFCqn2_tK>Ur)4FO;J(Q=L?I!d23uo
zUtL{2AfVQLA6OwBCnq6O%biQAyh+3(cM5uiWUO;EB8D-(4OLT&2l(X}-l*xS2OutI
zU^-eI$7N_sFD1ww*<<}(>EWvlaFj5FIKx+<7l4~1$)PqS@=f0q73a{~6cvif%Ixum
zfnL%w*^Ghpi0ii5M{&m*dA_C90s&;P|+W
zZmp>U3s!zOJ)kw4!y+i7ll+6Wegd`WIlYxGeGT85gx7GShbb}w$WH%P
z=GV_;%j@3bTOR!~W5`bsvh|go?ccQJ&!*lT#M}Jy3~wvF)5dxs=KB6BVFC`qZ^1RN
zhL1q(#!wyb58V@5u4q-r`={!?V^PjK2F)
zZ2&^R`8_Xue-Ap&D{3i?ZJZMO79dG)acY(LNs+>mDX=_Y9Pi*B
z$f7$CU~eT1d0!7_gnh8nldxQFj@(Cn2MXH*HLT6?=4*dw`Z*aF=H0cCPK?^&8
z=XC?NEy5q5u))M7TlMqF*8#u}WTM0ZQWHgf57AL_vSr89
zX`L?7F$YY^h7qe{;g23RcB!m=@nd6bMnNMVXTOK1SrE-VF#UW0*FXM<=$FF@@U~ms
zs*_WcIp}0BbE&=((5yEZgHK+E6
znK79h>kXyXJHv)hnN7Eh5mWUx*)NC}s*21`B_wE46%wS#V!tP;7Fi+oy8=Z-X`A(i
z^{6>8!*MCt$E#yuEEHt6En)Z5U1xxH|C!Vyl5vNM%dUdQiD&lH7s1lW>OjJ3%O4mI
zf5`*6t)|K%(HjD@B%0%sg}VbSV!8Mv>X`VLouia6s-+Kw#m*ToB4guKWU)Ux*{(fw
z=*rE71JyT#)X>aTjoBW0H9=VHHMeMb1=j5tHdq+9LBXuK4IA9szN>_|1o-kp>?pS{*}-;3c(ix8n>Us)!=Ve}K=hW6$@g
z34j7Mr#*3saeOGLwya_(0cAc6<_0{^dZzo?8LIc1GBF=)U7ht}6;E93d`<9YBPqu^
zDqy|{pp5vqMVqaUCJPy}Zj80g3Jn;O)3tT;r)j)l9B)LK{pP5A>*Xr*@bDKg9anvY
zv@^wr`)_)#+QI0}(t7B(xG-7MUxFd0(nea>%dpC}l}o`3fjc|XL0;~3XV^n)^CZ)l)wwUJOYxq|Q<7Va
zqRe%CYZ-f7H8A_(OQM`BZefLO=4$rYi1VA#+d*P&Ru+lvb7oU0)qH3?dE)wplnk+v
zk}K+wc3!Zds&w6E2HmP>&w#LaseagiM~@VSvv>CD$`4J_c`p)%D&!<8TOzqr;y~aR
z5tVOsnO&k}Y4tB&nTyX+V$&jj`SXgVu;mqwMq50{P
zNEfz{S&)1WJgT*AWpvF`WfOa!8nG1VnC6CT(Qa=WcOY}qYtb2+{z)fH6&ZiYIDDay
z*Vi~E{CDOTqo^5TXZyZ4Bxm{l&pKN772g6*?dgR?iCoHdb?228Bk$~W9C!V!ZVs@A
zThAHr*Z6-*m)B_@;s*g^L;dD<>MO_bo<@k)=zrcgIU08=u()lNEx;F%;mYF
zI$=avB!&l#o{m(rxZfosI+zcPh-jdHMjplvXDySgw;qg470V%KM_AC5!<&4aXe;tG
zCNcCLKx?-x4Xi+deg1HL2E~}kVS1fzJF6F9drGC;_(`OJqP=(N`q@$uRUN8qBe|dn
zqgt~>PW&6~!JC}GL0A#m9-!EOtJ>7VbjJDPQ=0s@urr@9pkRYSg-<9@&^^U&@Mvvs
z?%=m0D(E|Lb`Y_U@)sBROhk~7QL4MMa`n9G`RML3!iK{PU2X>_ED0sF#cysVst7N-
zKiLZXwy-KXa;aA`Rg9u64WIQ{Yv%o9FNahOHOrJ)Vc&=2{upfde_S7eAS9EMbmN^q
z0t6#ZagVjxEgPGb%wE*k3J4Q;q@mZxEQG=eU|E)vi=uz~It1yTAsU0QeWq$)?8~)R
zE{<9Qs?_r>jzvgRguiGNYFLbJ!PuM&_7dp+9St!kGr*h;6m!0?%HEfXua^o`X}qqZ
zsj?L{#FF{F!7acK+99Z6fAuN*|9qMNLvbhjZyoT6k0dI1qaZ#Zd{p&53WVE|JV%y_
zt8>s*a1H97xbMR-qG_mKo23hsudoX{GB<+tNe{TTW&et^S_HnVC3V)Qy3U%?DBI3Y
zGyUF0OSOz5DGH3B`lb@w%THeH4w;>u3m&|RQYQSde
z+a)HlP*O&AUCM({N=Pu4!cs!D!Ex;8`{HnTw0&$n
zD&t+PI#UZOAfjHt)=_x%;l)PLV0^Igg)7$)I{x!STBqk*nkYMHj-V!yj^BTu&(>v_@JZF&3aWJ
z$?WT%8>oc!-WK{X#
zM!p9|@VXFwjm)O=`6Pg3k>nY`qZ&&@ktbT+5S0=XZjVOn&Z_^-DD(KBVr$#p`BvI+
z%}?#FaCd2!^4p2*6UXN1?$IfHmiU@p;?@h2Vhg3&<(XWELoHQI6ZD~F&EJpLVW8o}
ze%x(;cf)?gV`YRC>#!m&a>*rufLCFR_#@Bh1N~L4=IwoaOYNfl2WHWBupVb0(g4v}
zDO{kOWq2pN<%Bl0qGG4?WzbT%#ohedNKN4K@^Ibm@9qy+FvQLPTY)?46}Fit@buG@
zs3P#;_!22s`jNxr`iJGfFI>A&-2tHRjgIig>{Fv@ZotTNA0uC2$os=+u|7|7N8t7rUHUBnR=8u
za`T+3Qmjtt00%c1u#x+WP9F>&J|gZ{YG`xmv5V~IFdx8L@>Hw%ZRCq@nwleeuW;4h9|
zx=i!0Zj(DA+lRNYpm{k-dngK4ornxOj5s9D*Xl8z*gfMn5lG~kLrLyUXCA)TP8tii
zq@*7wwC^Qk%hM(+vx2Hy38?dBdypWpDwaVC;$Zgu;
zl1ILQV?lcnE=^S6rs5MlfPoo)wzg*TQjG%Iw@wxu%7~9!4cwz_$JfSRVGCGtc`y65
zT$kYpVq}<#CrG_Mj6{1&Ve&Zk_ykZWweleqS}d3=CrU>unNWO(e8%8Q)PB*NC>X
zqt!45Z9IEqb7`!f%}qP=T{4#mQ|PdS@z!7F3m<2G+4b+=u>xr;mQ%M%uXwXqZ?CLj
z-}UTjeL?(EG6aDaRSJ*6_St78