summaryrefslogtreecommitdiff
path: root/app
diff options
context:
space:
mode:
Diffstat (limited to 'app')
-rw-r--r--app/assets/javascripts/clusters/clusters_bundle.js62
-rw-r--r--app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue7
-rw-r--r--app/assets/javascripts/clusters/stores/clusters_store.js1
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/gl_form.js87
-rw-r--r--app/assets/javascripts/helpers/indent_helper.js182
-rw-r--r--app/assets/javascripts/jobs/components/job_app.vue5
-rw-r--r--app/assets/javascripts/jobs/components/sidebar.vue9
-rw-r--r--app/assets/javascripts/jobs/index.js29
-rw-r--r--app/assets/javascripts/lib/utils/common_utils.js65
-rw-r--r--app/assets/javascripts/lib/utils/keycodes.js10
-rw-r--r--app/assets/javascripts/lib/utils/undo_stack.js105
-rw-r--r--app/assets/javascripts/manual_ordering.js2
-rw-r--r--app/assets/javascripts/notes/stores/utils.js2
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue2
-rw-r--r--app/assets/javascripts/performance_bar/components/detailed_metric.vue2
-rw-r--r--app/assets/javascripts/persistent_user_callout.js25
-rw-r--r--app/assets/javascripts/privacy_policy_update_callout.js8
-rw-r--r--app/assets/javascripts/projects/gke_cluster_namespace/index.js6
-rw-r--r--app/assets/javascripts/right_sidebar.js10
-rw-r--r--app/assets/javascripts/tracking.js67
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue2
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/toolbar.vue41
-rw-r--r--app/assets/stylesheets/application.scss1
-rw-r--r--app/assets/stylesheets/framework/common.scss1
-rw-r--r--app/assets/stylesheets/pages/builds.scss1
-rw-r--r--app/controllers/autocomplete_controller.rb2
-rw-r--r--app/controllers/boards/issues_controller.rb2
-rw-r--r--app/controllers/concerns/issuable_actions.rb22
-rw-r--r--app/controllers/concerns/notes_actions.rb2
-rw-r--r--app/controllers/groups_controller.rb4
-rw-r--r--app/controllers/projects/issues_controller.rb4
-rw-r--r--app/controllers/projects/notes_controller.rb2
-rw-r--r--app/controllers/snippets/notes_controller.rb4
-rw-r--r--app/finders/branches_finder.rb2
-rw-r--r--app/finders/container_repositories_finder.rb34
-rw-r--r--app/finders/notes_finder.rb42
-rw-r--r--app/helpers/application_settings_helper.rb2
-rw-r--r--app/models/application_setting_implementation.rb14
-rw-r--r--app/models/ci/build.rb14
-rw-r--r--app/models/ci/build_need.rb14
-rw-r--r--app/models/ci/pipeline.rb9
-rw-r--r--app/models/clusters/applications/cert_manager.rb36
-rw-r--r--app/models/clusters/applications/helm.rb28
-rw-r--r--app/models/clusters/applications/knative.rb10
-rw-r--r--app/models/clusters/applications/prometheus.rb10
-rw-r--r--app/models/commit_status.rb12
-rw-r--r--app/models/concerns/ci/metadatable.rb1
-rw-r--r--app/models/concerns/diff_positionable_note.rb8
-rw-r--r--app/models/concerns/group_descendant.rb4
-rw-r--r--app/models/concerns/relative_positioning.rb115
-rw-r--r--app/models/concerns/update_project_statistics.rb7
-rw-r--r--app/models/container_repository.rb6
-rw-r--r--app/models/diff_note.rb7
-rw-r--r--app/models/environment.rb2
-rw-r--r--app/models/group.rb2
-rw-r--r--app/models/hooks/system_hook.rb4
-rw-r--r--app/models/hooks/web_hook.rb6
-rw-r--r--app/models/merge_request.rb2
-rw-r--r--app/models/namespace/aggregation_schedule.rb14
-rw-r--r--app/models/note.rb4
-rw-r--r--app/models/project_statistics.rb6
-rw-r--r--app/policies/group_policy.rb1
-rw-r--r--app/serializers/analytics_issue_entity.rb2
-rw-r--r--app/serializers/user_serializer.rb17
-rw-r--r--app/services/application_settings/update_service.rb9
-rw-r--r--app/services/auth/container_registry_authentication_service.rb10
-rw-r--r--app/services/ci/process_pipeline_service.rb88
-rw-r--r--app/services/ci/retry_build_service.rb2
-rw-r--r--app/services/merge_requests/mergeability_check_service.rb45
-rw-r--r--app/services/self_monitoring/project/create_service.rb32
-rw-r--r--app/services/web_hook_service.rb6
-rw-r--r--app/validators/addressable_url_validator.rb2
-rw-r--r--app/validators/system_hook_url_validator.rb18
-rw-r--r--app/views/admin/application_settings/_outbound.html.haml10
-rw-r--r--app/views/clusters/clusters/_banner.html.haml5
-rw-r--r--app/views/clusters/clusters/show.html.haml49
-rw-r--r--app/views/explore/projects/_projects.html.haml3
-rw-r--r--app/views/explore/projects/trending.html.haml2
-rw-r--r--app/views/help/_shortcuts.html.haml10
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/projects/_flash_messages.html.haml2
-rw-r--r--app/views/projects/jobs/show.html.haml3
-rw-r--r--app/views/projects/mirrors/_instructions.html.haml2
-rw-r--r--app/views/projects/services/prometheus/_metrics.html.haml56
-rw-r--r--app/views/projects/services/prometheus/_show.html.haml9
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml2
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml5
-rw-r--r--app/views/shared/issuable/_sort_dropdown.html.haml2
-rw-r--r--app/views/shared/milestones/_top.html.haml2
-rw-r--r--app/views/shared/notes/_hints.html.haml11
-rw-r--r--app/views/shared/projects/_list.html.haml3
-rw-r--r--app/views/u2f/_register.html.haml2
-rw-r--r--app/workers/all_queues.yml1
-rw-r--r--app/workers/build_process_worker.rb16
-rw-r--r--app/workers/namespaces/root_statistics_worker.rb6
-rw-r--r--app/workers/namespaces/schedule_aggregation_worker.rb6
-rw-r--r--app/workers/pipeline_process_worker.rb7
98 files changed, 854 insertions, 783 deletions
diff --git a/app/assets/javascripts/clusters/clusters_bundle.js b/app/assets/javascripts/clusters/clusters_bundle.js
index aacfa0d87e6..5f5c8044b49 100644
--- a/app/assets/javascripts/clusters/clusters_bundle.js
+++ b/app/assets/javascripts/clusters/clusters_bundle.js
@@ -48,6 +48,9 @@ export default class Clusters {
} = document.querySelector('.js-edit-cluster-form').dataset;
this.clusterId = clusterId;
+ this.clusterNewlyCreatedKey = `cluster_${this.clusterId}_newly_created`;
+ this.clusterBannerDismissedKey = `cluster_${this.clusterId}_banner_dismissed`;
+
this.store = new ClustersStore();
this.store.setHelpPaths(helpPath, ingressHelpPath, ingressDnsHelpPath);
this.store.setManagePrometheusPath(managePrometheusPath);
@@ -81,18 +84,19 @@ export default class Clusters {
this.showTokenButton = document.querySelector('.js-show-cluster-token');
this.tokenField = document.querySelector('.js-cluster-token');
this.ingressDomainHelpText = document.querySelector('.js-ingress-domain-help-text');
- this.ingressDomainSnippet = this.ingressDomainHelpText.querySelector(
- '.js-ingress-domain-snippet',
- );
+ this.ingressDomainSnippet =
+ this.ingressDomainHelpText &&
+ this.ingressDomainHelpText.querySelector('.js-ingress-domain-snippet');
Clusters.initDismissableCallout();
initSettingsPanels();
- setupToggleButtons(document.querySelector('.js-cluster-enable-toggle-area'));
+ const toggleButtonsContainer = document.querySelector('.js-cluster-enable-toggle-area');
+ if (toggleButtonsContainer) {
+ setupToggleButtons(toggleButtonsContainer);
+ }
this.initApplications(clusterType);
- if (this.store.state.status !== 'created') {
- this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
- }
+ this.updateContainer(null, this.store.state.status, this.store.state.statusReason);
this.addListeners();
if (statusPath) {
@@ -247,35 +251,56 @@ export default class Clusters {
setBannerDismissedState(status, isDismissed) {
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- window.localStorage.setItem(
- `cluster_${this.clusterId}_banner_dismissed`,
- `${status}_${isDismissed}`,
- );
+ window.localStorage.setItem(this.clusterBannerDismissedKey, `${status}_${isDismissed}`);
}
}
isBannerDismissed(status) {
let bannerState;
if (AccessorUtilities.isLocalStorageAccessSafe()) {
- bannerState = window.localStorage.getItem(`cluster_${this.clusterId}_banner_dismissed`);
+ bannerState = window.localStorage.getItem(this.clusterBannerDismissedKey);
}
return bannerState === `${status}_true`;
}
- updateContainer(prevStatus, status, error) {
- this.hideAll();
+ setClusterNewlyCreated(state) {
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ window.localStorage.setItem(this.clusterNewlyCreatedKey, Boolean(state));
+ }
+ }
+
+ isClusterNewlyCreated() {
+ // once this is true, it will always be true for a given page load
+ if (!this.isNewlyCreated) {
+ let newlyCreated;
+ if (AccessorUtilities.isLocalStorageAccessSafe()) {
+ newlyCreated = window.localStorage.getItem(this.clusterNewlyCreatedKey);
+ }
+
+ this.isNewlyCreated = newlyCreated === 'true';
+ }
+ return this.isNewlyCreated;
+ }
- if (this.isBannerDismissed(status)) {
+ updateContainer(prevStatus, status, error) {
+ if (status !== 'created' && this.isBannerDismissed(status)) {
return;
}
this.setBannerDismissedState(status, false);
- // We poll all the time but only want the `created` banner to show when newly created
- if (this.store.state.status !== 'created' || prevStatus !== this.store.state.status) {
+ if (prevStatus !== status) {
+ this.hideAll();
+
switch (status) {
case 'created':
- this.successContainer.classList.remove('hidden');
+ if (this.isClusterNewlyCreated()) {
+ this.setClusterNewlyCreated(false);
+ this.successContainer.classList.remove('hidden');
+ } else if (prevStatus) {
+ this.setClusterNewlyCreated(true);
+ window.location.reload();
+ }
break;
case 'errored':
this.errorContainer.classList.remove('hidden');
@@ -292,7 +317,6 @@ export default class Clusters {
this.creatingContainer.classList.remove('hidden');
break;
default:
- this.hideAll();
}
}
}
diff --git a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
index 2ff6d5e32e2..4f60e543666 100644
--- a/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
+++ b/app/assets/javascripts/clusters/components/uninstall_application_confirmation_modal.vue
@@ -2,14 +2,17 @@
import { GlModal } from '@gitlab/ui';
import { sprintf, s__ } from '~/locale';
import trackUninstallButtonClickMixin from 'ee_else_ce/clusters/mixins/track_uninstall_button_click';
-import { INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
+import { HELM, INGRESS, CERT_MANAGER, PROMETHEUS, RUNNER, KNATIVE, JUPYTER } from '../constants';
const CUSTOM_APP_WARNING_TEXT = {
+ [HELM]: s__(
+ 'ClusterIntegration|The associated Tiller pod will be deleted and cannot be restored.',
+ ),
[INGRESS]: s__(
'ClusterIntegration|The associated load balancer and IP will be deleted and cannot be restored.',
),
[CERT_MANAGER]: s__(
- 'ClusterIntegration|The associated certifcate will be deleted and cannot be restored.',
+ 'ClusterIntegration|The associated private key will be deleted and cannot be restored.',
),
[PROMETHEUS]: s__('ClusterIntegration|All data will be deleted and cannot be restored.'),
[RUNNER]: s__('ClusterIntegration|Any running pipelines will be canceled.'),
diff --git a/app/assets/javascripts/clusters/stores/clusters_store.js b/app/assets/javascripts/clusters/stores/clusters_store.js
index f64f0ca616f..ada5a49e246 100644
--- a/app/assets/javascripts/clusters/stores/clusters_store.js
+++ b/app/assets/javascripts/clusters/stores/clusters_store.js
@@ -171,6 +171,7 @@ export default class ClusterStore {
this.state.applications.cert_manager.email || serverAppEntry.email;
} else if (appId === JUPYTER) {
this.state.applications.jupyter.hostname =
+ this.state.applications.jupyter.hostname ||
serverAppEntry.hostname ||
(this.state.applications.ingress.externalIp
? `jupyter.${this.state.applications.ingress.externalIp}.nip.io`
diff --git a/app/assets/javascripts/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index daa941a63cd..7a6ad3dc771 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -12,7 +12,6 @@ import 'core-js/es/promise/finally';
import 'core-js/es/string/code-point-at';
import 'core-js/es/string/from-code-point';
import 'core-js/es/string/includes';
-import 'core-js/es/string/repeat';
import 'core-js/es/string/starts-with';
import 'core-js/es/string/ends-with';
import 'core-js/es/symbol';
diff --git a/app/assets/javascripts/gl_form.js b/app/assets/javascripts/gl_form.js
index b98fe9f6ce2..a66555838ba 100644
--- a/app/assets/javascripts/gl_form.js
+++ b/app/assets/javascripts/gl_form.js
@@ -3,16 +3,9 @@ import autosize from 'autosize';
import GfmAutoComplete, { defaultAutocompleteConfig } from 'ee_else_ce/gfm_auto_complete';
import dropzoneInput from './dropzone_input';
import { addMarkdownListeners, removeMarkdownListeners } from './lib/utils/text_markdown';
-import IndentHelper from './helpers/indent_helper';
-import { keystroke } from './lib/utils/common_utils';
-import * as keys from './lib/utils/keycodes';
-import UndoStack from './lib/utils/undo_stack';
export default class GLForm {
constructor(form, enableGFM = {}) {
- this.handleKeyShortcuts = this.handleKeyShortcuts.bind(this);
- this.setState = this.setState.bind(this);
-
this.form = form;
this.textarea = this.form.find('textarea.js-gfm-input');
this.enableGFM = Object.assign({}, defaultAutocompleteConfig, enableGFM);
@@ -23,10 +16,6 @@ export default class GLForm {
this.enableGFM[item] = Boolean(dataSources[item]);
}
});
-
- this.undoStack = new UndoStack();
- this.indentHelper = new IndentHelper(this.textarea[0]);
-
// Before we start, we should clean up any previous data for this form
this.destroy();
// Set up the form
@@ -96,84 +85,9 @@ export default class GLForm {
clearEventListeners() {
this.textarea.off('focus');
this.textarea.off('blur');
- this.textarea.off('keydown');
removeMarkdownListeners(this.form);
}
- setState(state) {
- const selection = [this.textarea[0].selectionStart, this.textarea[0].selectionEnd];
- this.textarea.val(state);
- this.textarea[0].setSelectionRange(selection[0], selection[1]);
- }
-
- /*
- Handle keypresses for a custom undo/redo stack.
- We need this because the toolbar buttons and indentation helpers mess with the browser's
- native undo/redo capability.
- */
- handleUndo(event) {
- const content = this.textarea.val();
- const { selectionStart, selectionEnd } = this.textarea[0];
- const stack = this.undoStack;
-
- if (stack.isEmpty()) {
- // ==== Save initial state in undo history ====
- stack.save(content);
- }
-
- if (keystroke(event, keys.Z_KEY_CODE, 'l')) {
- // ==== Undo ====
- event.preventDefault();
- stack.save(content);
- if (stack.canUndo()) {
- this.setState(stack.undo());
- }
- } else if (keystroke(event, keys.Z_KEY_CODE, 'ls') || keystroke(event, keys.Y_KEY_CODE, 'l')) {
- // ==== Redo ====
- event.preventDefault();
- if (stack.canRedo()) {
- this.setState(stack.redo());
- }
- } else if (
- keystroke(event, keys.SPACE_KEY_CODE) ||
- keystroke(event, keys.ENTER_KEY_CODE) ||
- selectionStart !== selectionEnd
- ) {
- // ==== Save after finishing a word or before deleting a large selection ====
- stack.save(content);
- } else if (content === '') {
- // ==== Save after deleting everything ====
- stack.save('');
- } else {
- // ==== Save after 1 second of inactivity ====
- stack.scheduleSave(content);
- }
- }
-
- handleIndent(event) {
- if (keystroke(event, keys.LEFT_BRACKET_KEY_CODE, 'l')) {
- // ==== Unindent selected lines ====
- event.preventDefault();
- this.indentHelper.unindent();
- } else if (keystroke(event, keys.RIGHT_BRACKET_KEY_CODE, 'l')) {
- // ==== Indent selected lines ====
- event.preventDefault();
- this.indentHelper.indent();
- } else if (keystroke(event, keys.ENTER_KEY_CODE)) {
- // ==== Auto-indent new lines ====
- event.preventDefault();
- this.indentHelper.newline();
- } else if (keystroke(event, keys.BACKSPACE_KEY_CODE)) {
- // ==== Auto-delete indents at the beginning of the line ====
- this.indentHelper.backspace(event);
- }
- }
-
- handleKeyShortcuts(event) {
- this.handleIndent(event);
- this.handleUndo(event);
- }
-
addEventListeners() {
this.textarea.on('focus', function focusTextArea() {
$(this)
@@ -185,6 +99,5 @@ export default class GLForm {
.closest('.md-area')
.removeClass('is-focused');
});
- this.textarea.on('keydown', e => this.handleKeyShortcuts(e.originalEvent));
}
}
diff --git a/app/assets/javascripts/helpers/indent_helper.js b/app/assets/javascripts/helpers/indent_helper.js
deleted file mode 100644
index a8815fac04e..00000000000
--- a/app/assets/javascripts/helpers/indent_helper.js
+++ /dev/null
@@ -1,182 +0,0 @@
-const INDENT_SEQUENCE = ' ';
-
-function countLeftSpaces(text) {
- const i = text.split('').findIndex(c => c !== ' ');
- return i === -1 ? text.length : i;
-}
-
-/**
- * IndentHelper provides methods that allow manual and smart indentation in
- * textareas. It supports line indent/unindent, selection indent/unindent,
- * auto indentation on newlines, and smart deletion of indents with backspace.
- */
-export default class IndentHelper {
- /**
- * Creates a new IndentHelper and binds it to the given `textarea`. You can provide a custom indent sequence in the second parameter, but the `newline` and `backspace` operations may work funny if the indent sequence isn't spaces only.
- */
- constructor(textarea, indentSequence = INDENT_SEQUENCE) {
- this.element = textarea;
- this.seq = indentSequence;
- }
-
- getSelection() {
- return { start: this.element.selectionStart, end: this.element.selectionEnd };
- }
-
- isRangeSelection() {
- return this.element.selectionStart !== this.element.selectionEnd;
- }
-
- /**
- * Re-implementation of textarea's setRangeText method, because IE/Edge don't support it.
- *
- * @see https://html.spec.whatwg.org/multipage/form-control-infrastructure.html#dom-textarea%2Finput-setrangetext
- */
- setRangeText(replacement, start, end, selectMode) {
- // Disable eslint to remain as faithful as possible to the above linked spec
- /* eslint-disable no-param-reassign, no-case-declarations */
- const text = this.element.value;
-
- if (start > end) {
- throw new RangeError('setRangeText: start index must be less than or equal to end index');
- }
-
- // Clamp to [0, len]
- start = Math.max(0, Math.min(start, text.length));
- end = Math.max(0, Math.min(end, text.length));
-
- let selection = { start: this.element.selectionStart, end: this.element.selectionEnd };
-
- this.element.value = text.slice(0, start) + replacement + text.slice(end);
-
- const newLength = replacement.length;
- const newEnd = start + newLength;
-
- switch (selectMode) {
- case 'select':
- selection = { start, newEnd };
- break;
- case 'start':
- selection = { start, end: start };
- break;
- case 'end':
- selection = { start: newEnd, end: newEnd };
- break;
- case 'preserve':
- default:
- const oldLength = end - start;
- const delta = newLength - oldLength;
- if (selection.start > end) {
- selection.start += delta;
- } else if (selection.start > start) {
- selection.start = start;
- }
- if (selection.end > end) {
- selection.end += delta;
- } else if (selection.end > start) {
- selection.end = newEnd;
- }
- }
-
- this.element.setSelectionRange(selection.start, selection.end);
-
- /* eslint-enable no-param-reassign, no-case-declarations */
- }
-
- /**
- * Returns an array of lines in the textarea, with information about their
- * start/end offsets and whether they are included in the current selection.
- */
- splitLines() {
- const { start, end } = this.getSelection();
-
- const lines = this.element.value.split('\n');
- let textStart = 0;
- const lineObjects = [];
- lines.forEach(line => {
- const lineObj = {
- text: line,
- start: textStart,
- end: textStart + line.length,
- };
- lineObj.inSelection = lineObj.start <= end && lineObj.end >= start;
- lineObjects.push(lineObj);
- textStart += line.length + 1;
- });
- return lineObjects;
- }
-
- /**
- * Indents selected lines by one level.
- */
- indent() {
- const { start } = this.getSelection();
-
- const selectedLines = this.splitLines().filter(line => line.inSelection);
- if (!this.isRangeSelection() && start === selectedLines[0].start) {
- // Special case: if cursor is at the beginning of the line, move it one
- // indent right.
- const line = selectedLines[0];
- this.setRangeText(this.seq, line.start, line.start, 'end');
- } else {
- selectedLines.reverse();
- selectedLines.forEach(line => {
- this.setRangeText(INDENT_SEQUENCE, line.start, line.start, 'preserve');
- });
- }
- }
-
- /**
- * Unindents selected lines by one level.
- */
- unindent() {
- const lines = this.splitLines().filter(line => line.inSelection);
- lines.reverse();
- lines
- .filter(line => line.text.startsWith(this.seq))
- .forEach(line => {
- this.setRangeText('', line.start, line.start + this.seq.length, 'preserve');
- });
- }
-
- /**
- * Emulates a newline keypress, automatically indenting the new line.
- */
- newline() {
- const { start, end } = this.getSelection();
-
- if (this.isRangeSelection()) {
- // Manually kill the selection before calculating the indent
- this.setRangeText('', start, end, 'start');
- }
-
- // Auto-indent the next line
- const currentLine = this.splitLines().find(line => line.end >= start);
- const spaces = countLeftSpaces(currentLine.text);
- this.setRangeText(`\n${' '.repeat(spaces)}`, start, start, 'end');
- }
-
- /**
- * If the cursor is positioned at the end of a line's leading indents,
- * emulates a backspace keypress by deleting a single level of indents.
- * @param event The DOM KeyboardEvent that triggers this action, or null.
- */
- backspace(event) {
- const { start } = this.getSelection();
-
- // If the cursor is at the end of leading indents, delete an indent.
- if (!this.isRangeSelection()) {
- const currentLine = this.splitLines().find(line => line.end >= start);
- const cursorPosition = start - currentLine.start;
- if (countLeftSpaces(currentLine.text) === cursorPosition && cursorPosition > 0) {
- if (event) event.preventDefault();
-
- let spacesToDelete = cursorPosition % this.seq.length;
- if (spacesToDelete === 0) {
- spacesToDelete = this.seq.length;
- }
- this.setRangeText('', start - spacesToDelete, start, 'start');
- }
- }
- }
-}
diff --git a/app/assets/javascripts/jobs/components/job_app.vue b/app/assets/javascripts/jobs/components/job_app.vue
index ef9fb6d08d1..8da87f424c4 100644
--- a/app/assets/javascripts/jobs/components/job_app.vue
+++ b/app/assets/javascripts/jobs/components/job_app.vue
@@ -73,6 +73,10 @@ export default {
type: String,
required: true,
},
+ projectPath: {
+ type: String,
+ required: true,
+ },
logState: {
type: String,
required: true,
@@ -258,6 +262,7 @@ export default {
:quota-used="job.runners.quota.used"
:quota-limit="job.runners.quota.limit"
:runners-path="runnerHelpUrl"
+ :project-path="projectPath"
/>
<environments-block
diff --git a/app/assets/javascripts/jobs/components/sidebar.vue b/app/assets/javascripts/jobs/components/sidebar.vue
index e9704584c9f..06477477aad 100644
--- a/app/assets/javascripts/jobs/components/sidebar.vue
+++ b/app/assets/javascripts/jobs/components/sidebar.vue
@@ -73,15 +73,14 @@ export default {
},
renderBlock() {
return (
- this.job.merge_request ||
this.job.duration ||
- this.job.finished_data ||
+ this.job.finished_at ||
this.job.erased_at ||
this.job.queued ||
+ this.hasTimeout ||
this.job.runner ||
this.job.coverage ||
- this.job.tags.length ||
- this.job.cancel_path
+ this.job.tags.length
);
},
hasArtifact() {
@@ -160,7 +159,7 @@ export default {
</gl-link>
</div>
- <div :class="{ block: renderBlock }">
+ <div v-if="renderBlock" class="block">
<detail-row
v-if="job.duration"
:value="duration"
diff --git a/app/assets/javascripts/jobs/index.js b/app/assets/javascripts/jobs/index.js
index 06514fcce1d..add7f9b710a 100644
--- a/app/assets/javascripts/jobs/index.js
+++ b/app/assets/javascripts/jobs/index.js
@@ -10,16 +10,29 @@ export default () => {
JobApp,
},
render(createElement) {
+ const {
+ deploymentHelpUrl,
+ runnerHelpUrl,
+ runnerSettingsUrl,
+ variablesSettingsUrl,
+ endpoint,
+ pagePath,
+ logState,
+ buildStatus,
+ projectPath,
+ } = element.dataset;
+
return createElement('job-app', {
props: {
- deploymentHelpUrl: element.dataset.deploymentHelpUrl,
- runnerHelpUrl: element.dataset.runnerHelpUrl,
- runnerSettingsUrl: element.dataset.runnerSettingsUrl,
- variablesSettingsUrl: element.dataset.variablesSettingsUrl,
- endpoint: element.dataset.endpoint,
- pagePath: element.dataset.buildOptionsPagePath,
- logState: element.dataset.buildOptionsLogState,
- buildStatus: element.dataset.buildOptionsBuildStatus,
+ deploymentHelpUrl,
+ runnerHelpUrl,
+ runnerSettingsUrl,
+ variablesSettingsUrl,
+ endpoint,
+ pagePath,
+ logState,
+ buildStatus,
+ projectPath,
},
});
},
diff --git a/app/assets/javascripts/lib/utils/common_utils.js b/app/assets/javascripts/lib/utils/common_utils.js
index 1a94aee2398..5e90893b684 100644
--- a/app/assets/javascripts/lib/utils/common_utils.js
+++ b/app/assets/javascripts/lib/utils/common_utils.js
@@ -203,71 +203,6 @@ export const isMetaKey = e => e.metaKey || e.ctrlKey || e.altKey || e.shiftKey;
// 3) Middle-click or Mouse Wheel Click (e.which is 2)
export const isMetaClick = e => e.metaKey || e.ctrlKey || e.which === 2;
-export const getPlatformLeaderKey = () => {
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- if (navigator && navigator.platform && navigator.platform.startsWith('Mac')) {
- return 'meta';
- }
- return 'ctrl';
-};
-
-export const getPlatformLeaderKeyHTML = () => {
- if (getPlatformLeaderKey() === 'meta') {
- return '&#8984;';
- }
- // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
- return 'Ctrl';
-};
-
-export const isPlatformLeaderKey = e => {
- if (getPlatformLeaderKey() === 'meta') {
- return Boolean(e.metaKey);
- }
- return Boolean(e.ctrlKey);
-};
-
-/**
- * Tests if a KeyboardEvent corresponds exactly to a keystroke.
- *
- * This function avoids hacking around an old version of Mousetrap, which we ship at the moment. It should be removed after we upgrade to the newest Mousetrap. See:
- * - https://gitlab.com/gitlab-org/gitlab-ce/issues/63182
- * - https://gitlab.com/gitlab-org/gitlab-ce/issues/64246
- *
- * @example
- * // Matches the enter key with exactly zero modifiers
- * keystroke(event, 13)
- *
- * @example
- * // Matches Control-Shift-Z
- * keystroke(event, 90, 'cs')
- *
- * @param e The KeyboardEvent to test.
- * @param keyCode The key code of the key to test. Why keycodes? IE/Edge don't support the more convenient `key` and `code` properties.
- * @param modifiers A string of modifiers keys. Each modifier key is represented by one character. The set of pressed modifier keys must match the given string exactly. Available options are 'a' for Alt/Option, 'c' for Control, 'm' for Meta/Command, 's' for Shift, and 'l' for the leader key (Meta on MacOS and Control otherwise).
- * @returns {boolean} True if the KeyboardEvent corresponds to the given keystroke.
- */
-export const keystroke = (e, keyCode, modifiers = '') => {
- if (!e || !keyCode) {
- return false;
- }
-
- const leader = getPlatformLeaderKey();
- const mods = modifiers.toLowerCase().replace('l', leader.charAt(0));
-
- // Match depressed modifier keys
- if (
- e.altKey !== mods.includes('a') ||
- e.ctrlKey !== mods.includes('c') ||
- e.metaKey !== mods.includes('m') ||
- e.shiftKey !== mods.includes('s')
- ) {
- return false;
- }
-
- // Match the depressed key
- return keyCode === (e.keyCode || e.which);
-};
-
export const contentTop = () => {
const perfBar = $('#js-peek').outerHeight() || 0;
const mrTabsHeight = $('.merge-request-tabs').outerHeight() || 0;
diff --git a/app/assets/javascripts/lib/utils/keycodes.js b/app/assets/javascripts/lib/utils/keycodes.js
index e24fcf47d71..5e0f9b612a2 100644
--- a/app/assets/javascripts/lib/utils/keycodes.js
+++ b/app/assets/javascripts/lib/utils/keycodes.js
@@ -1,10 +1,4 @@
-export const BACKSPACE_KEY_CODE = 8;
-export const ENTER_KEY_CODE = 13;
-export const ESC_KEY_CODE = 27;
-export const SPACE_KEY_CODE = 32;
export const UP_KEY_CODE = 38;
export const DOWN_KEY_CODE = 40;
-export const Y_KEY_CODE = 89;
-export const Z_KEY_CODE = 90;
-export const LEFT_BRACKET_KEY_CODE = 219;
-export const RIGHT_BRACKET_KEY_CODE = 221;
+export const ENTER_KEY_CODE = 13;
+export const ESC_KEY_CODE = 27;
diff --git a/app/assets/javascripts/lib/utils/undo_stack.js b/app/assets/javascripts/lib/utils/undo_stack.js
deleted file mode 100644
index 6cfdc2a0a0f..00000000000
--- a/app/assets/javascripts/lib/utils/undo_stack.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/**
- * UndoStack provides a custom implementation of an undo/redo engine. It was originally written for GitLab's Markdown editor (`gl_form.js`), whose rich text editing capabilities broke native browser undo/redo behaviour.
- *
- * UndoStack supports predictable undos/redos, debounced saves, maximum history length, and duplicate detection.
- *
- * Usage:
- * - `stack = new UndoStack();`
- * - Saves a state to the stack with `stack.save(state)`.
- * - Get the current state with `stack.current()`.
- * - Revert to the previous state with `stack.undo()`.
- * - Redo a previous undo with `stack.redo()`;
- * - Queue a future save with `stack.scheduleSave(state, delay)`. Useful for text editors.
- * - See the full undo history in `stack.history`.
- */
-export default class UndoStack {
- constructor(maxLength = 1000) {
- this.clear();
- this.maxLength = maxLength;
-
- // If you're storing reference-types in the undo stack, you might want to
- // reassign this property to some deep-equals function.
- this.comparator = (a, b) => a === b;
- }
-
- current() {
- if (this.cursor === -1) {
- return undefined;
- }
- return this.history[this.cursor];
- }
-
- isEmpty() {
- return this.history.length === 0;
- }
-
- clear() {
- this.clearPending();
- this.history = [];
- this.cursor = -1;
- }
-
- save(state) {
- this.clearPending();
- if (this.comparator(state, this.current())) {
- // Don't save state if it's the same as the current state
- return;
- }
-
- this.history.length = this.cursor + 1;
- this.history.push(state);
- this.cursor += 1;
-
- if (this.history.length > this.maxLength) {
- this.history.shift();
- this.cursor -= 1;
- }
- }
-
- scheduleSave(state, delay = 1000) {
- this.clearPending();
- this.pendingState = state;
- this.timeout = setTimeout(this.saveNow.bind(this), delay);
- }
-
- saveNow() {
- // Persists scheduled saves immediately
- this.save(this.pendingState);
- this.clearPending();
- }
-
- clearPending() {
- // Cancels any scheduled saves
- if (this.timeout) {
- clearTimeout(this.timeout);
- delete this.timeout;
- delete this.pendingState;
- }
- }
-
- canUndo() {
- return this.cursor > 0;
- }
-
- undo() {
- this.clearPending();
- if (!this.canUndo()) {
- return undefined;
- }
- this.cursor -= 1;
- return this.history[this.cursor];
- }
-
- canRedo() {
- return this.cursor >= 0 && this.cursor < this.history.length - 1;
- }
-
- redo() {
- this.clearPending();
- if (!this.canRedo()) {
- return undefined;
- }
- this.cursor += 1;
- return this.history[this.cursor];
- }
-}
diff --git a/app/assets/javascripts/manual_ordering.js b/app/assets/javascripts/manual_ordering.js
index 012d1e70410..29a0e5a904a 100644
--- a/app/assets/javascripts/manual_ordering.js
+++ b/app/assets/javascripts/manual_ordering.js
@@ -21,7 +21,7 @@ const updateIssue = (url, issueList, { move_before_id, move_after_id }) =>
const initManualOrdering = () => {
const issueList = document.querySelector('.manual-ordering');
- if (!issueList || !(gon.features && gon.features.manualSorting) || !(gon.current_user_id > 0)) {
+ if (!issueList || !(gon.current_user_id > 0)) {
return;
}
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index ed4cef4a917..97dcd54fe88 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -21,7 +21,7 @@ export const getQuickActionText = note => {
text = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- text = sprintf(__('Applying command to %{commandDescription}', { commandDescription }));
+ text = sprintf(__('Applying command to %{commandDescription}'), { commandDescription });
}
}
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
index ed518611d0b..3c5de189d51 100644
--- a/app/assets/javascripts/operation_settings/components/external_dashboard.vue
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -50,9 +50,11 @@ export default {
<form>
<gl-form-group
:label="s__('ExternalMetrics|Full dashboard URL')"
+ label-for="full-dashboard-url"
:description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
>
<gl-form-input
+ id="full-dashboard-url"
v-model="userDashboardUrl"
placeholder="https://my-org.gitlab.io/my-dashboards"
@keydown.enter.native.prevent="updateExternalDashboardUrl"
diff --git a/app/assets/javascripts/performance_bar/components/detailed_metric.vue b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
index 5bc1d5e0533..73524827c5d 100644
--- a/app/assets/javascripts/performance_bar/components/detailed_metric.vue
+++ b/app/assets/javascripts/performance_bar/components/detailed_metric.vue
@@ -44,7 +44,7 @@ export default {
</script>
<template>
<div
- v-if="currentRequest.details"
+ v-if="currentRequest.details && metricDetails"
:id="`peek-view-${metric}`"
class="view qa-performance-bar-detailed-metric"
>
diff --git a/app/assets/javascripts/persistent_user_callout.js b/app/assets/javascripts/persistent_user_callout.js
index 4a08e158f6b..8d6a3781048 100644
--- a/app/assets/javascripts/persistent_user_callout.js
+++ b/app/assets/javascripts/persistent_user_callout.js
@@ -1,13 +1,17 @@
+import { parseBoolean } from './lib/utils/common_utils';
import axios from './lib/utils/axios_utils';
import { __ } from './locale';
import Flash from './flash';
+const DEFERRED_LINK_CLASS = 'deferred-link';
+
export default class PersistentUserCallout {
constructor(container) {
- const { dismissEndpoint, featureId } = container.dataset;
+ const { dismissEndpoint, featureId, deferLinks } = container.dataset;
this.container = container;
this.dismissEndpoint = dismissEndpoint;
this.featureId = featureId;
+ this.deferLinks = parseBoolean(deferLinks);
this.init();
}
@@ -15,9 +19,21 @@ export default class PersistentUserCallout {
init() {
const closeButton = this.container.querySelector('.js-close');
closeButton.addEventListener('click', event => this.dismiss(event));
+
+ if (this.deferLinks) {
+ this.container.addEventListener('click', event => {
+ const isDeferredLink = event.target.classList.contains(DEFERRED_LINK_CLASS);
+
+ if (isDeferredLink) {
+ const { href, target } = event.target;
+
+ this.dismiss(event, { href, target });
+ }
+ });
+ }
}
- dismiss(event) {
+ dismiss(event, deferredLinkOptions = null) {
event.preventDefault();
axios
@@ -26,6 +42,11 @@ export default class PersistentUserCallout {
})
.then(() => {
this.container.remove();
+
+ if (deferredLinkOptions) {
+ const { href, target } = deferredLinkOptions;
+ window.open(href, target);
+ }
})
.catch(() => {
Flash(__('An error occurred while dismissing the alert. Refresh the page and try again.'));
diff --git a/app/assets/javascripts/privacy_policy_update_callout.js b/app/assets/javascripts/privacy_policy_update_callout.js
new file mode 100644
index 00000000000..126b1ee1132
--- /dev/null
+++ b/app/assets/javascripts/privacy_policy_update_callout.js
@@ -0,0 +1,8 @@
+import PersistentUserCallout from '~/persistent_user_callout';
+
+function initPrivacyPolicyUpdateCallout() {
+ const callout = document.querySelector('.privacy-policy-update-64341');
+ PersistentUserCallout.factory(callout);
+}
+
+export default initPrivacyPolicyUpdateCallout;
diff --git a/app/assets/javascripts/projects/gke_cluster_namespace/index.js b/app/assets/javascripts/projects/gke_cluster_namespace/index.js
index 288740203ad..0ec4d8807b0 100644
--- a/app/assets/javascripts/projects/gke_cluster_namespace/index.js
+++ b/app/assets/javascripts/projects/gke_cluster_namespace/index.js
@@ -28,8 +28,10 @@ const setState = glManagedCheckbox => {
const initGkeNamespace = () => {
const glManagedCheckbox = document.querySelector('.js-gl-managed');
- setState(glManagedCheckbox); // this is needed in order to set the initial state
- glManagedCheckbox.addEventListener('change', () => setState(glManagedCheckbox));
+ if (glManagedCheckbox) {
+ setState(glManagedCheckbox); // this is needed in order to set the initial state
+ glManagedCheckbox.addEventListener('change', () => setState(glManagedCheckbox));
+ }
};
export default initGkeNamespace;
diff --git a/app/assets/javascripts/right_sidebar.js b/app/assets/javascripts/right_sidebar.js
index 930c0d5e958..40a2158de78 100644
--- a/app/assets/javascripts/right_sidebar.js
+++ b/app/assets/javascripts/right_sidebar.js
@@ -101,10 +101,12 @@ Sidebar.prototype.toggleTodo = function(e) {
this.todoUpdateDone(data);
})
.catch(() =>
- flash(sprintf(__('There was an error %{message} todo.')), {
- message:
- ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'),
- }),
+ flash(
+ sprintf(__('There was an error %{message} todo.'), {
+ message:
+ ajaxType === 'post' ? s__('RightSidebar|adding a') : s__('RightSidebar|deleting the'),
+ }),
+ ),
);
};
diff --git a/app/assets/javascripts/tracking.js b/app/assets/javascripts/tracking.js
new file mode 100644
index 00000000000..2d0b099cf0b
--- /dev/null
+++ b/app/assets/javascripts/tracking.js
@@ -0,0 +1,67 @@
+import $ from 'jquery';
+
+const extractData = (el, opts = {}) => {
+ const { trackEvent, trackLabel = '', trackProperty = '' } = el.dataset;
+ let trackValue = el.dataset.trackValue || el.value || '';
+ if (el.type === 'checkbox' && !el.checked) trackValue = false;
+ return [
+ trackEvent + (opts.suffix || ''),
+ {
+ label: trackLabel,
+ property: trackProperty,
+ value: trackValue,
+ },
+ ];
+};
+
+export default class Tracking {
+ static enabled() {
+ return typeof window.snowplow === 'function';
+ }
+
+ static event(category = document.body.dataset.page, event = 'generic', data = {}) {
+ if (!this.enabled()) return false;
+ // eslint-disable-next-line @gitlab/i18n/no-non-i18n-strings
+ if (!category) throw new Error('Tracking: no category provided for tracking.');
+
+ return window.snowplow(
+ 'trackStructEvent',
+ category,
+ event,
+ Object.assign({}, { label: '', property: '', value: '' }, data),
+ );
+ }
+
+ constructor(category = document.body.dataset.page) {
+ this.category = category;
+ }
+
+ bind(container = document) {
+ if (!this.constructor.enabled()) return;
+ container.querySelectorAll(`[data-track-event]`).forEach(el => {
+ if (this.customHandlingFor(el)) return;
+ // jquery is required for select2, so we use it always
+ // see: https://github.com/select2/select2/issues/4686
+ $(el).on('click', this.eventHandler(this.category));
+ });
+ }
+
+ customHandlingFor(el) {
+ const classes = el.classList;
+
+ // bootstrap dropdowns
+ if (classes.contains('dropdown')) {
+ $(el).on('show.bs.dropdown', this.eventHandler(this.category, { suffix: '_show' }));
+ $(el).on('hide.bs.dropdown', this.eventHandler(this.category, { suffix: '_hide' }));
+ return true;
+ }
+
+ return false;
+ }
+
+ eventHandler(category = null, opts = {}) {
+ return e => {
+ this.constructor.event(category || this.category, ...extractData(e.currentTarget, opts));
+ };
+ }
+}
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
index 17ac8ada32d..76b96c8c1c0 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline_container.vue
@@ -60,7 +60,7 @@ export default {
return this.isPostMerge ? this.mr.mergePipeline : this.mr.pipeline;
},
showVisualReviewAppLink() {
- return Boolean(this.mr.visualReviewFF && this.mr.visualReviewAppAvailable);
+ return this.mr.visualReviewAppAvailable;
},
showMergeTrainInfo() {
return _.isNumber(this.mr.mergeTrainIndex);
diff --git a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
index 21c44b59520..8ce5b615795 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/toolbar.vue
@@ -1,6 +1,5 @@
<script>
import { GlLink } from '@gitlab/ui';
-import { s__, sprintf } from '~/locale';
export default {
components: {
@@ -23,28 +22,8 @@ export default {
},
},
computed: {
- toolbarHelpHtml() {
- const mdLinkStart = `<a href="${this.markdownDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`;
- const actionsLinkStart = `<a href="${this.quickActionsDocsPath}" target="_blank" rel="noopener noreferrer" tabindex="-1">`;
- const linkEnd = '</a>';
-
- if (this.markdownDocsPath && !this.quickActionsDocsPath) {
- return sprintf(
- s__('Editor|%{mdLinkStart}Markdown is supported%{mdLinkEnd}'),
- { mdLinkStart, mdLinkEnd: linkEnd },
- false,
- );
- } else if (this.markdownDocsPath && this.quickActionsDocsPath) {
- return sprintf(
- s__(
- 'Editor|%{mdLinkStart}Markdown%{mdLinkEnd} and %{actionsLinkStart}quick actions%{actionsLinkEnd} are supported',
- ),
- { mdLinkStart, mdLinkEnd: linkEnd, actionsLinkStart, actionsLinkEnd: linkEnd },
- false,
- );
- }
-
- return null;
+ hasQuickActionsDocsPath() {
+ return this.quickActionsDocsPath !== '';
},
},
};
@@ -53,7 +32,21 @@ export default {
<template>
<div class="comment-toolbar clearfix">
<div class="toolbar-text">
- <span v-html="toolbarHelpHtml"></span>
+ <template v-if="!hasQuickActionsDocsPath && markdownDocsPath">
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{
+ __('Markdown is supported')
+ }}</gl-link>
+ </template>
+ <template v-if="hasQuickActionsDocsPath && markdownDocsPath">
+ <gl-link :href="markdownDocsPath" target="_blank" tabindex="-1">{{
+ __('Markdown')
+ }}</gl-link>
+ and
+ <gl-link :href="quickActionsDocsPath" target="_blank" tabindex="-1">{{
+ __('quick actions')
+ }}</gl-link>
+ are supported
+ </template>
</div>
<span v-if="canAttachFile" class="uploading-container">
<span class="uploading-progress-container hide">
diff --git a/app/assets/stylesheets/application.scss b/app/assets/stylesheets/application.scss
index fbf16aa324a..e98030f1511 100644
--- a/app/assets/stylesheets/application.scss
+++ b/app/assets/stylesheets/application.scss
@@ -12,7 +12,6 @@
// If you need to add unique style that should affect only one page - use pages/
// directory.
@import "at.js/dist/css/jquery.atwho";
-@import "pikaday/scss/pikaday";
@import "dropzone/dist/basic";
@import "select2/select2";
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 6b44834cc52..f384a49e0ae 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -435,6 +435,7 @@ img.emoji {
/** COMMON SIZING CLASSES **/
.w-0 { width: 0; }
.w-8em { width: 8em; }
+.w-3rem { width: 3rem; }
.h-12em { height: 12em; }
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index 6e98908eeed..262c0bf5ed2 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -127,6 +127,7 @@
.section-header ~ .section.line {
margin-left: $gl-padding;
+ display: block;
}
}
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 091327931c2..f111c7ca8cc 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -16,7 +16,7 @@ class AutocompleteController < ApplicationController
.new(params: params, current_user: current_user, project: project, group: group)
.execute
- render json: UserSerializer.new.represent(users)
+ render json: UserSerializer.new(params).represent(users, project: project)
end
def user
diff --git a/app/controllers/boards/issues_controller.rb b/app/controllers/boards/issues_controller.rb
index 90528f75ffd..1d1a72d21f1 100644
--- a/app/controllers/boards/issues_controller.rb
+++ b/app/controllers/boards/issues_controller.rb
@@ -26,7 +26,7 @@ module Boards
list_service = Boards::Issues::ListService.new(board_parent, current_user, filter_params)
issues = list_service.execute
issues = issues.page(params[:page]).per(params[:per] || 20).without_count
- Issue.move_to_end(issues) if Gitlab::Database.read_write?
+ Issue.move_nulls_to_end(issues) if Gitlab::Database.read_write?
issues = issues.preload(:milestone,
:assignees,
project: [
diff --git a/app/controllers/concerns/issuable_actions.rb b/app/controllers/concerns/issuable_actions.rb
index 6fa2f75be33..398cb728e05 100644
--- a/app/controllers/concerns/issuable_actions.rb
+++ b/app/controllers/concerns/issuable_actions.rb
@@ -98,13 +98,12 @@ module IssuableActions
render json: { notice: "#{quantity} #{resource_name.pluralize(quantity)} updated" }
end
- # rubocop: disable CodeReuse/ActiveRecord
+ # rubocop:disable CodeReuse/ActiveRecord
def discussions
- notes = issuable.discussion_notes
- .inc_relations_for_view
- .with_notes_filter(notes_filter)
- .includes(:noteable)
- .fresh
+ notes = NotesFinder.new(current_user, finder_params_for_issuable).execute
+ .inc_relations_for_view
+ .includes(:noteable)
+ .fresh
if notes_filter != UserPreference::NOTES_FILTERS[:only_comments]
notes = ResourceEvents::MergeIntoNotesService.new(issuable, current_user).execute(notes)
@@ -117,7 +116,7 @@ module IssuableActions
render json: discussion_serializer.represent(discussions, context: self)
end
- # rubocop: enable CodeReuse/ActiveRecord
+ # rubocop:enable CodeReuse/ActiveRecord
private
@@ -222,4 +221,13 @@ module IssuableActions
def parent
@project || @group # rubocop:disable Gitlab/ModuleWithInstanceVariables
end
+
+ # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ def finder_params_for_issuable
+ {
+ target: @issuable,
+ notes_filter: notes_filter
+ }.tap { |new_params| new_params[:project] = project if respond_to?(:project, true) }
+ end
+ # rubocop:enable Gitlab/ModuleWithInstanceVariables
end
diff --git a/app/controllers/concerns/notes_actions.rb b/app/controllers/concerns/notes_actions.rb
index 0098c4cdf4c..d2a961efff7 100644
--- a/app/controllers/concerns/notes_actions.rb
+++ b/app/controllers/concerns/notes_actions.rb
@@ -243,7 +243,7 @@ module NotesActions
end
def notes_finder
- @notes_finder ||= NotesFinder.new(project, current_user, finder_params)
+ @notes_finder ||= NotesFinder.new(current_user, finder_params)
end
def note_serializer
diff --git a/app/controllers/groups_controller.rb b/app/controllers/groups_controller.rb
index dda321bac79..5472ef05d7c 100644
--- a/app/controllers/groups_controller.rb
+++ b/app/controllers/groups_controller.rb
@@ -7,10 +7,6 @@ class GroupsController < Groups::ApplicationController
include PreviewMarkdown
include RecordUserLastActivity
- before_action do
- push_frontend_feature_flag(:manual_sorting, default_enabled: true)
- end
-
respond_to :html
prepend_before_action(only: [:show, :issues]) { authenticate_sessionless_user!(:rss) }
diff --git a/app/controllers/projects/issues_controller.rb b/app/controllers/projects/issues_controller.rb
index db7ca7ef0d7..bc9166b9df3 100644
--- a/app/controllers/projects/issues_controller.rb
+++ b/app/controllers/projects/issues_controller.rb
@@ -10,10 +10,6 @@ class Projects::IssuesController < Projects::ApplicationController
include SpammableActions
include RecordUserLastActivity
- before_action do
- push_frontend_feature_flag(:manual_sorting, default_enabled: true)
- end
-
def issue_except_actions
%i[index calendar new create bulk_update import_csv]
end
diff --git a/app/controllers/projects/notes_controller.rb b/app/controllers/projects/notes_controller.rb
index 3152a38fd8e..65d9b074eee 100644
--- a/app/controllers/projects/notes_controller.rb
+++ b/app/controllers/projects/notes_controller.rb
@@ -68,7 +68,7 @@ class Projects::NotesController < Projects::ApplicationController
alias_method :awardable, :note
def finder_params
- params.merge(last_fetched_at: last_fetched_at, notes_filter: notes_filter)
+ params.merge(project: project, last_fetched_at: last_fetched_at, notes_filter: notes_filter)
end
def authorize_admin_note!
diff --git a/app/controllers/snippets/notes_controller.rb b/app/controllers/snippets/notes_controller.rb
index 612897f27e6..551b37cb3d3 100644
--- a/app/controllers/snippets/notes_controller.rb
+++ b/app/controllers/snippets/notes_controller.rb
@@ -27,7 +27,9 @@ class Snippets::NotesController < ApplicationController
alias_method :noteable, :snippet
def finder_params
- params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet')
+ params.merge(last_fetched_at: last_fetched_at, target_id: snippet.id, target_type: 'personal_snippet').tap do |merged_params|
+ merged_params[:project] = project if respond_to?(:project)
+ end
end
def authorize_read_snippet!
diff --git a/app/finders/branches_finder.rb b/app/finders/branches_finder.rb
index b462c8053fa..291a24c1405 100644
--- a/app/finders/branches_finder.rb
+++ b/app/finders/branches_finder.rb
@@ -69,7 +69,7 @@ class BranchesFinder
return branches unless names
branch_names = names.to_set
- branches.filter do |branch|
+ branches.select do |branch|
branch_names.include?(branch.name)
end
end
diff --git a/app/finders/container_repositories_finder.rb b/app/finders/container_repositories_finder.rb
new file mode 100644
index 00000000000..eb91d7f825b
--- /dev/null
+++ b/app/finders/container_repositories_finder.rb
@@ -0,0 +1,34 @@
+# frozen_string_literal: true
+
+class ContainerRepositoriesFinder
+ # id: group or project id
+ # container_type: :group or :project
+ def initialize(id:, container_type:)
+ @id = id
+ @type = container_type.to_sym
+ end
+
+ def execute
+ if project_type?
+ project.container_repositories
+ else
+ group.container_repositories
+ end
+ end
+
+ private
+
+ attr_reader :id, :type
+
+ def project_type?
+ type == :project
+ end
+
+ def project
+ Project.find(id)
+ end
+
+ def group
+ Group.find(id)
+ end
+end
diff --git a/app/finders/notes_finder.rb b/app/finders/notes_finder.rb
index 8f610d7dddb..f7d9100bb78 100644
--- a/app/finders/notes_finder.rb
+++ b/app/finders/notes_finder.rb
@@ -3,6 +3,8 @@
class NotesFinder
FETCH_OVERLAP = 5.seconds
+ attr_reader :target_type
+
# Used to filter Notes
# When used with target_type and target_id this returns notes specifically for the controller
#
@@ -10,15 +12,17 @@ class NotesFinder
# current_user - which user check authorizations with
# project - which project to look for notes on
# params:
+ # target: noteable
# target_type: string
# target_id: integer
# last_fetched_at: time
# search: string
#
- def initialize(project, current_user, params = {})
- @project = project
+ def initialize(current_user, params = {})
+ @project = params[:project]
@current_user = current_user
- @params = params
+ @params = params.dup
+ @target_type = @params[:target_type]
end
def execute
@@ -32,7 +36,27 @@ class NotesFinder
def target
return @target if defined?(@target)
- target_type = @params[:target_type]
+ if target_given?
+ use_explicit_target
+ else
+ find_target_by_type_and_ids
+ end
+ end
+
+ private
+
+ def target_given?
+ @params.key?(:target)
+ end
+
+ def use_explicit_target
+ @target = @params[:target]
+ @target_type = @target.class.name.underscore
+
+ @target
+ end
+
+ def find_target_by_type_and_ids
target_id = @params[:target_id]
target_iid = @params[:target_iid]
@@ -45,13 +69,11 @@ class NotesFinder
@project.commit(target_id)
end
else
- noteables_for_type_by_id(target_type, target_id, target_iid)
+ noteable_for_type_by_id(target_type, target_id, target_iid)
end
end
- private
-
- def noteables_for_type_by_id(type, id, iid)
+ def noteable_for_type_by_id(type, id, iid)
query = if id
{ id: id }
else
@@ -77,10 +99,6 @@ class NotesFinder
search(notes)
end
- def target_type
- @params[:target_type]
- end
-
# rubocop: disable CodeReuse/ActiveRecord
def notes_of_any_type
types = %w(commit issue merge_request snippet)
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 3847a35fbab..acbcf0ded17 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -160,6 +160,8 @@ module ApplicationSettingsHelper
:akismet_api_key,
:akismet_enabled,
:allow_local_requests_from_hooks_and_services,
+ :allow_local_requests_from_web_hooks_and_services,
+ :allow_local_requests_from_system_hooks,
:dns_rebinding_protection_enabled,
:archive_builds_in_human_readable,
:authorized_keys_enabled,
diff --git a/app/models/application_setting_implementation.rb b/app/models/application_setting_implementation.rb
index 6efd07a6008..b7a4d7aa803 100644
--- a/app/models/application_setting_implementation.rb
+++ b/app/models/application_setting_implementation.rb
@@ -21,7 +21,8 @@ module ApplicationSettingImplementation
{
after_sign_up_text: nil,
akismet_enabled: false,
- allow_local_requests_from_hooks_and_services: false,
+ allow_local_requests_from_web_hooks_and_services: false,
+ allow_local_requests_from_system_hooks: true,
dns_rebinding_protection_enabled: true,
authorized_keys_enabled: true, # TODO default to false if the instance is configured to use AuthorizedKeysCommand
container_registry_token_expire_delay: 5,
@@ -158,9 +159,20 @@ module ApplicationSettingImplementation
end
def outbound_local_requests_whitelist_raw=(values)
+ clear_memoization(:outbound_local_requests_whitelist_arrays)
+
self.outbound_local_requests_whitelist = domain_strings_to_array(values)
end
+ def add_to_outbound_local_requests_whitelist(values_array)
+ clear_memoization(:outbound_local_requests_whitelist_arrays)
+
+ self.outbound_local_requests_whitelist ||= []
+ self.outbound_local_requests_whitelist += values_array
+
+ self.outbound_local_requests_whitelist.uniq!
+ end
+
def outbound_local_requests_whitelist_arrays
strong_memoize(:outbound_local_requests_whitelist_arrays) do
next [[], []] unless self.outbound_local_requests_whitelist
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 07813e03f3a..ac88d9714ac 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -38,6 +38,7 @@ module Ci
has_one :deployment, as: :deployable, class_name: 'Deployment'
has_many :trace_sections, class_name: 'Ci::BuildTraceSection'
has_many :trace_chunks, class_name: 'Ci::BuildTraceChunk', foreign_key: :build_id
+ has_many :needs, class_name: 'Ci::BuildNeed', foreign_key: :build_id, inverse_of: :build
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
@@ -50,6 +51,7 @@ module Ci
accepts_nested_attributes_for :runner_session
accepts_nested_attributes_for :job_variables
+ accepts_nested_attributes_for :needs
delegate :url, to: :runner_session, prefix: true, allow_nil: true
delegate :terminal_specification, to: :runner_session, allow_nil: true
@@ -713,11 +715,17 @@ module Ci
depended_jobs = depends_on_builds
- return depended_jobs unless options[:dependencies].present?
+ # find all jobs that are needed
+ if Feature.enabled?(:ci_dag_support, project) && needs.exists?
+ depended_jobs = depended_jobs.where(name: needs.select(:name))
+ end
- depended_jobs.select do |job|
- options[:dependencies].include?(job.name)
+ # find all jobs that are dependent on
+ if options[:dependencies].present?
+ depended_jobs = depended_jobs.where(name: options[:dependencies])
end
+
+ depended_jobs
end
def empty_dependencies?
diff --git a/app/models/ci/build_need.rb b/app/models/ci/build_need.rb
new file mode 100644
index 00000000000..6531dfd332f
--- /dev/null
+++ b/app/models/ci/build_need.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+module Ci
+ class BuildNeed < ApplicationRecord
+ extend Gitlab::Ci::Model
+
+ belongs_to :build, class_name: "Ci::Build", foreign_key: :build_id, inverse_of: :needs
+
+ validates :build, presence: true
+ validates :name, presence: true, length: { maximum: 128 }
+
+ scope :scoped_build, -> { where('ci_builds.id=ci_build_needs.build_id') }
+ end
+end
diff --git a/app/models/ci/pipeline.rb b/app/models/ci/pipeline.rb
index 1c76f401690..3b28eb246db 100644
--- a/app/models/ci/pipeline.rb
+++ b/app/models/ci/pipeline.rb
@@ -504,8 +504,9 @@ module Ci
return [] unless config_processor
strong_memoize(:stage_seeds) do
- seeds = config_processor.stages_attributes.map do |attributes|
- Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes)
+ seeds = config_processor.stages_attributes.inject([]) do |previous_stages, attributes|
+ seed = Gitlab::Ci::Pipeline::Seed::Stage.new(self, attributes, previous_stages)
+ previous_stages + [seed]
end
seeds.select(&:included?)
@@ -611,8 +612,8 @@ module Ci
end
# rubocop: disable CodeReuse/ServiceClass
- def process!
- Ci::ProcessPipelineService.new(project, user).execute(self)
+ def process!(trigger_build_ids = nil)
+ Ci::ProcessPipelineService.new(project, user).execute(self, trigger_build_ids)
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/clusters/applications/cert_manager.rb b/app/models/clusters/applications/cert_manager.rb
index 7d5a6dec519..2fc1b67dfd2 100644
--- a/app/models/clusters/applications/cert_manager.rb
+++ b/app/models/clusters/applications/cert_manager.rb
@@ -24,12 +24,6 @@ module Clusters
'stable/cert-manager'
end
- # We will implement this in future MRs.
- # Need to reverse postinstall step
- def allowed_to_uninstall?
- false
- end
-
def install_command
Gitlab::Kubernetes::Helm::InstallCommand.new(
name: 'certmanager',
@@ -41,12 +35,42 @@ module Clusters
)
end
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::DeleteCommand.new(
+ name: 'certmanager',
+ rbac: cluster.platform_kubernetes_rbac?,
+ files: files,
+ postdelete: post_delete_script
+ )
+ end
+
private
def post_install_script
["kubectl create -f /data/helm/certmanager/config/cluster_issuer.yaml"]
end
+ def post_delete_script
+ [
+ delete_private_key,
+ delete_crd('certificates.certmanager.k8s.io'),
+ delete_crd('clusterissuers.certmanager.k8s.io'),
+ delete_crd('issuers.certmanager.k8s.io')
+ ].compact
+ end
+
+ def private_key_name
+ @private_key_name ||= cluster_issuer_content.dig('spec', 'acme', 'privateKeySecretRef', 'name')
+ end
+
+ def delete_private_key
+ "kubectl delete secret -n #{Gitlab::Kubernetes::Helm::NAMESPACE} #{private_key_name} --ignore-not-found" if private_key_name.present?
+ end
+
+ def delete_crd(definition)
+ "kubectl delete crd #{definition} --ignore-not-found"
+ end
+
def cluster_issuer_file
{
'cluster_issuer.yaml': cluster_issuer_yaml_content
diff --git a/app/models/clusters/applications/helm.rb b/app/models/clusters/applications/helm.rb
index a83d06c4b00..3a175fec148 100644
--- a/app/models/clusters/applications/helm.rb
+++ b/app/models/clusters/applications/helm.rb
@@ -14,6 +14,7 @@ module Clusters
include ::Clusters::Concerns::ApplicationCore
include ::Clusters::Concerns::ApplicationStatus
+ include ::Gitlab::Utils::StrongMemoize
default_value_for :version, Gitlab::Kubernetes::Helm::HELM_VERSION
@@ -29,11 +30,22 @@ module Clusters
self.status = 'installable' if cluster&.platform_kubernetes_active?
end
- # We will implement this in future MRs.
- # Basically we need to check all other applications are not installed
- # first.
+ # It can only be uninstalled if there are no other applications installed
+ # or with intermitent installation statuses in the database.
def allowed_to_uninstall?
- false
+ strong_memoize(:allowed_to_uninstall) do
+ applications = nil
+
+ Clusters::Cluster::APPLICATIONS.each do |application_name, klass|
+ next if application_name == 'helm'
+
+ extra_apps = Clusters::Applications::Helm.where('EXISTS (?)', klass.select(1).where(cluster_id: cluster_id))
+
+ applications = applications.present? ? applications.or(extra_apps) : extra_apps
+ end
+
+ !applications.exists?
+ end
end
def install_command
@@ -44,6 +56,14 @@ module Clusters
)
end
+ def uninstall_command
+ Gitlab::Kubernetes::Helm::ResetCommand.new(
+ name: name,
+ files: files,
+ rbac: cluster.platform_kubernetes_rbac?
+ )
+ end
+
def has_ssl?
ca_key.present? && ca_cert.present?
end
diff --git a/app/models/clusters/applications/knative.rb b/app/models/clusters/applications/knative.rb
index 96f526e8a36..5eae23659ae 100644
--- a/app/models/clusters/applications/knative.rb
+++ b/app/models/clusters/applications/knative.rb
@@ -84,7 +84,7 @@ module Clusters
private
def delete_knative_services_and_metrics
- delete_knative_services + delete_knative_istio_metrics.to_a
+ delete_knative_services + delete_knative_istio_metrics
end
def delete_knative_services
@@ -117,11 +117,15 @@ module Clusters
end
def install_knative_metrics
- ["kubectl apply -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
+ return [] unless cluster.application_prometheus_available?
+
+ ["kubectl apply -f #{METRICS_CONFIG}"]
end
def delete_knative_istio_metrics
- ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"] if cluster.application_prometheus_available?
+ return [] unless cluster.application_prometheus_available?
+
+ ["kubectl delete --ignore-not-found -f #{METRICS_CONFIG}"]
end
def verify_cluster?
diff --git a/app/models/clusters/applications/prometheus.rb b/app/models/clusters/applications/prometheus.rb
index f5375d29f3a..5eb535cab58 100644
--- a/app/models/clusters/applications/prometheus.rb
+++ b/app/models/clusters/applications/prometheus.rb
@@ -64,7 +64,7 @@ module Clusters
name: name,
rbac: cluster.platform_kubernetes_rbac?,
files: files,
- predelete: delete_knative_istio_metrics.to_a
+ predelete: delete_knative_istio_metrics
)
end
@@ -104,11 +104,15 @@ module Clusters
end
def install_knative_metrics
- ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available?
+ return [] unless cluster.application_knative_available?
+
+ ["kubectl apply -f #{Clusters::Applications::Knative::METRICS_CONFIG}"]
end
def delete_knative_istio_metrics
- ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"] if cluster.application_knative_available?
+ return [] unless cluster.application_knative_available?
+
+ ["kubectl delete -f #{Clusters::Applications::Knative::METRICS_CONFIG}"]
end
end
end
diff --git a/app/models/commit_status.rb b/app/models/commit_status.rb
index be6f3e9c5b0..a88cac6b8e6 100644
--- a/app/models/commit_status.rb
+++ b/app/models/commit_status.rb
@@ -43,6 +43,16 @@ class CommitStatus < ApplicationRecord
scope :after_stage, -> (index) { where('stage_idx > ?', index) }
scope :processables, -> { where(type: %w[Ci::Build Ci::Bridge]) }
+ scope :with_needs, -> (names = nil) do
+ needs = Ci::BuildNeed.scoped_build.select(1)
+ needs = needs.where(name: names) if names
+ where('EXISTS (?)', needs).preload(:needs)
+ end
+
+ scope :without_needs, -> do
+ where('NOT EXISTS (?)', Ci::BuildNeed.scoped_build.select(1))
+ end
+
# We use `CommitStatusEnums.failure_reasons` here so that EE can more easily
# extend this `Hash` with new values.
enum_with_nil failure_reason: ::CommitStatusEnums.failure_reasons
@@ -116,7 +126,7 @@ class CommitStatus < ApplicationRecord
commit_status.run_after_commit do
if pipeline_id
if complete? || manual?
- PipelineProcessWorker.perform_async(pipeline_id)
+ PipelineProcessWorker.perform_async(pipeline_id, [id])
else
PipelineUpdateWorker.perform_async(pipeline_id)
end
diff --git a/app/models/concerns/ci/metadatable.rb b/app/models/concerns/ci/metadatable.rb
index 9eed9492b37..304cc71e9dc 100644
--- a/app/models/concerns/ci/metadatable.rb
+++ b/app/models/concerns/ci/metadatable.rb
@@ -29,6 +29,7 @@ module Ci
def degenerate!
self.class.transaction do
self.update!(options: nil, yaml_variables: nil)
+ self.needs.all.delete_all
self.metadata&.destroy
end
end
diff --git a/app/models/concerns/diff_positionable_note.rb b/app/models/concerns/diff_positionable_note.rb
index 2d09eff0111..195d9e107c5 100644
--- a/app/models/concerns/diff_positionable_note.rb
+++ b/app/models/concerns/diff_positionable_note.rb
@@ -10,6 +10,8 @@ module DiffPositionableNote
serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiveRecordSerialize
+
+ validate :diff_refs_match_commit, if: :for_commit?
end
%i(original_position position change_position).each do |meth|
@@ -71,4 +73,10 @@ module DiffPositionableNote
self.position = result[:position]
end
end
+
+ def diff_refs_match_commit
+ return if self.original_position.diff_refs == commit&.diff_refs
+
+ errors.add(:commit_id, 'does not match the diff refs')
+ end
end
diff --git a/app/models/concerns/group_descendant.rb b/app/models/concerns/group_descendant.rb
index cfffd845e43..ed14b73ac1b 100644
--- a/app/models/concerns/group_descendant.rb
+++ b/app/models/concerns/group_descendant.rb
@@ -42,7 +42,7 @@ module GroupDescendant
parent = child.parent
exception = ArgumentError.new <<~MSG
- parent: [GroupDescendant: #{parent.inspect}] was not preloaded for [#{child.inspect}]")
+ Parent was not preloaded for child when rendering group hierarchy.
This error is not user facing, but causes a +1 query.
MSG
extras = {
@@ -50,7 +50,7 @@ module GroupDescendant
child: child.inspect,
preloaded: preloaded.map(&:full_path)
}
- issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/40785'
+ issue_url = 'https://gitlab.com/gitlab-org/gitlab-ce/issues/49404'
Gitlab::Sentry.track_exception(exception, issue_url: issue_url, extra: extras)
end
diff --git a/app/models/concerns/relative_positioning.rb b/app/models/concerns/relative_positioning.rb
index 9cd7b8d6258..6d3c7a7ed68 100644
--- a/app/models/concerns/relative_positioning.rb
+++ b/app/models/concerns/relative_positioning.rb
@@ -29,12 +29,8 @@ module RelativePositioning
MAX_POSITION = Gitlab::Database::MAX_INT_VALUE
IDEAL_DISTANCE = 500
- included do
- after_save :save_positionable_neighbours
- end
-
class_methods do
- def move_to_end(objects)
+ def move_nulls_to_end(objects)
objects = objects.reject(&:relative_position)
return if objects.empty?
@@ -43,7 +39,7 @@ module RelativePositioning
self.transaction do
objects.each do |object|
- relative_position = position_between(max_relative_position, MAX_POSITION)
+ relative_position = position_between(max_relative_position || START_POSITION, MAX_POSITION)
object.relative_position = relative_position
max_relative_position = relative_position
object.save(touch: false)
@@ -114,11 +110,12 @@ module RelativePositioning
return move_after(before) unless after
return move_before(after) unless before
- # If there is no place to insert an item we need to create one by moving the before item closer
- # to its predecessor. This process will recursively move all the predecessors until we have a place
+ # If there is no place to insert an item we need to create one by moving the item
+ # before this and all preceding items until there is a gap
+ before, after = after, before if after.relative_position < before.relative_position
if (after.relative_position - before.relative_position) < 2
- before.move_before
- @positionable_neighbours = [before] # rubocop:disable Gitlab/ModuleWithInstanceVariables
+ after.move_sequence_before
+ before.reset
end
self.relative_position = self.class.position_between(before.relative_position, after.relative_position)
@@ -128,12 +125,8 @@ module RelativePositioning
pos_before = before.relative_position
pos_after = before.next_relative_position
- if before.shift_after?
- item_to_move = self.class.relative_positioning_query_base(self).find_by!(relative_position: pos_after)
- item_to_move.move_after
- @positionable_neighbours = [item_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- pos_after = item_to_move.relative_position
+ if pos_after && (pos_after - pos_before) < 2
+ before.move_sequence_after
end
self.relative_position = self.class.position_between(pos_before, pos_after)
@@ -143,12 +136,8 @@ module RelativePositioning
pos_after = after.relative_position
pos_before = after.prev_relative_position
- if after.shift_before?
- item_to_move = self.class.relative_positioning_query_base(self).find_by!(relative_position: pos_before)
- item_to_move.move_before
- @positionable_neighbours = [item_to_move] # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- pos_before = item_to_move.relative_position
+ if pos_before && (pos_after - pos_before) < 2
+ after.move_sequence_before
end
self.relative_position = self.class.position_between(pos_before, pos_after)
@@ -162,36 +151,82 @@ module RelativePositioning
self.relative_position = self.class.position_between(min_relative_position || START_POSITION, MIN_POSITION)
end
- # Indicates if there is an item that should be shifted to free the place
- def shift_after?
- next_pos = next_relative_position
- next_pos && (next_pos - relative_position) == 1
+ # Moves the sequence before the current item to the middle of the next gap
+ # For example, we have 5 11 12 13 14 15 and the current item is 15
+ # This moves the sequence 11 12 13 14 to 8 9 10 11
+ def move_sequence_before
+ next_gap = find_next_gap_before
+ delta = optimum_delta_for_gap(next_gap)
+
+ move_sequence(next_gap[:start], relative_position, -delta)
end
- # Indicates if there is an item that should be shifted to free the place
- def shift_before?
- prev_pos = prev_relative_position
- prev_pos && (relative_position - prev_pos) == 1
+ # Moves the sequence after the current item to the middle of the next gap
+ # For example, we have 11 12 13 14 15 21 and the current item is 11
+ # This moves the sequence 12 13 14 15 to 15 16 17 18
+ def move_sequence_after
+ next_gap = find_next_gap_after
+ delta = optimum_delta_for_gap(next_gap)
+
+ move_sequence(relative_position, next_gap[:start], delta)
end
private
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
- def save_positionable_neighbours
- return unless @positionable_neighbours
+ # Supposing that we have a sequence of items: 1 5 11 12 13 and the current item is 13
+ # This would return: `{ start: 11, end: 5 }`
+ def find_next_gap_before
+ items_with_next_pos = scoped_items
+ .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position DESC) AS next_pos')
+ .where('relative_position <= ?', relative_position)
+ .order(relative_position: :desc)
+
+ find_next_gap(items_with_next_pos).tap do |gap|
+ gap[:end] ||= MIN_POSITION
+ end
+ end
+
+ # Supposing that we have a sequence of items: 13 14 15 20 24 and the current item is 13
+ # This would return: `{ start: 15, end: 20 }`
+ def find_next_gap_after
+ items_with_next_pos = scoped_items
+ .select('relative_position AS pos, LEAD(relative_position) OVER (ORDER BY relative_position ASC) AS next_pos')
+ .where('relative_position >= ?', relative_position)
+ .order(:relative_position)
- status = @positionable_neighbours.all? { |item| item.save(touch: false) }
- @positionable_neighbours = nil
+ find_next_gap(items_with_next_pos).tap do |gap|
+ gap[:end] ||= MAX_POSITION
+ end
+ end
+
+ def find_next_gap(items_with_next_pos)
+ gap = self.class.from(items_with_next_pos, :items_with_next_pos)
+ .where('ABS(pos - next_pos) > 1 OR next_pos IS NULL')
+ .limit(1)
+ .pluck(:pos, :next_pos)
+ .first
+
+ { start: gap[0], end: gap[1] }
+ end
- status
+ def optimum_delta_for_gap(gap)
+ delta = ((gap[:start] - gap[:end]) / 2.0).abs.ceil
+
+ [delta, IDEAL_DISTANCE].min
+ end
+
+ def move_sequence(start_pos, end_pos, delta)
+ scoped_items
+ .where.not(id: self.id)
+ .where('relative_position BETWEEN ? AND ?', start_pos, end_pos)
+ .update_all("relative_position = relative_position + #{delta}")
end
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
def calculate_relative_position(calculation)
# When calculating across projects, this is much more efficient than
# MAX(relative_position) without the GROUP BY, due to index usage:
# https://gitlab.com/gitlab-org/gitlab-ce/issues/54276#note_119340977
- relation = self.class.relative_positioning_query_base(self)
+ relation = scoped_items
.order(Gitlab::Database.nulls_last_order('position', 'DESC'))
.group(self.class.relative_positioning_parent_column)
.limit(1)
@@ -203,4 +238,8 @@ module RelativePositioning
.first&.
last
end
+
+ def scoped_items
+ self.class.relative_positioning_query_base(self)
+ end
end
diff --git a/app/models/concerns/update_project_statistics.rb b/app/models/concerns/update_project_statistics.rb
index 570a735973f..869b3490f3f 100644
--- a/app/models/concerns/update_project_statistics.rb
+++ b/app/models/concerns/update_project_statistics.rb
@@ -73,15 +73,10 @@ module UpdateProjectStatistics
def schedule_namespace_aggregation_worker
run_after_commit do
- next unless schedule_aggregation_worker?
+ next if project.nil?
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end
end
-
- def schedule_aggregation_worker?
- !project.nil? &&
- Feature.enabled?(:update_statistics_namespace, project.root_ancestor)
- end
end
end
diff --git a/app/models/container_repository.rb b/app/models/container_repository.rb
index facd81dde80..2a5ae7930e6 100644
--- a/app/models/container_repository.rb
+++ b/app/models/container_repository.rb
@@ -70,10 +70,14 @@ class ContainerRepository < ApplicationRecord
digests = tags.map { |tag| tag.digest }.to_set
digests.all? do |digest|
- client.delete_repository_tag(self.path, digest)
+ delete_tag_by_digest(digest)
end
end
+ def delete_tag_by_digest(digest)
+ client.delete_repository_tag(self.path, digest)
+ end
+
def self.build_from_path(path)
self.new(project: path.repository_project,
name: path.repository_name)
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index f75c32633b1..861185dc222 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -20,7 +20,6 @@ class DiffNote < Note
validates :noteable_type, inclusion: { in: -> (_note) { noteable_types } }
validate :positions_complete
validate :verify_supported
- validate :diff_refs_match_commit, if: :for_commit?
before_validation :set_line_code, if: :on_text?
after_save :keep_around_commits
@@ -154,12 +153,6 @@ class DiffNote < Note
errors.add(:position, "is invalid")
end
- def diff_refs_match_commit
- return if self.original_position.diff_refs == self.commit.diff_refs
-
- errors.add(:commit_id, 'does not match the diff refs')
- end
-
def keep_around_commits
shas = [
self.original_position.base_sha,
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 392481ea0cc..513427ac2c5 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -204,7 +204,7 @@ class Environment < ApplicationRecord
public_path = project.public_path_for_source_path(path, commit_sha)
return unless public_path
- [external_url, public_path].join('/')
+ [external_url.delete_suffix('/'), public_path.delete_prefix('/')].join('/')
end
def expire_etag_cache
diff --git a/app/models/group.rb b/app/models/group.rb
index 74eb556b1b5..6c868b1d1f0 100644
--- a/app/models/group.rb
+++ b/app/models/group.rb
@@ -44,6 +44,8 @@ class Group < Namespace
has_many :cluster_groups, class_name: 'Clusters::Group'
has_many :clusters, through: :cluster_groups, class_name: 'Clusters::Cluster'
+ has_many :container_repositories, through: :projects
+
has_many :todos
accepts_nested_attributes_for :variables, allow_destroy: true
diff --git a/app/models/hooks/system_hook.rb b/app/models/hooks/system_hook.rb
index 90b4588a325..3d54d17e787 100644
--- a/app/models/hooks/system_hook.rb
+++ b/app/models/hooks/system_hook.rb
@@ -14,8 +14,10 @@ class SystemHook < WebHook
default_value_for :repository_update_events, true
default_value_for :merge_requests_events, false
+ validates :url, system_hook_url: true
+
# Allow urls pointing localhost and the local network
def allow_local_requests?
- true
+ Gitlab::CurrentSettings.allow_local_requests_from_system_hooks?
end
end
diff --git a/app/models/hooks/web_hook.rb b/app/models/hooks/web_hook.rb
index daf7ff4b771..16fc7fdbd48 100644
--- a/app/models/hooks/web_hook.rb
+++ b/app/models/hooks/web_hook.rb
@@ -15,8 +15,8 @@ class WebHook < ApplicationRecord
has_many :web_hook_logs, dependent: :destroy # rubocop:disable Cop/ActiveRecordDependent
- validates :url, presence: true, public_url: { allow_localhost: lambda(&:allow_local_requests?),
- allow_local_network: lambda(&:allow_local_requests?) }
+ validates :url, presence: true
+ validates :url, public_url: true, unless: ->(hook) { hook.is_a?(SystemHook) }
validates :token, format: { without: /\n/ }
validates :push_events_branch_filter, branch_filter: true
@@ -35,6 +35,6 @@ class WebHook < ApplicationRecord
# Allow urls pointing localhost and the local network
def allow_local_requests?
- false
+ Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
end
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 8ade91933a4..5e8a6a7d5e5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -752,7 +752,7 @@ class MergeRequest < ApplicationRecord
end
def check_mergeability
- MergeRequests::MergeabilityCheckService.new(self).execute
+ MergeRequests::MergeabilityCheckService.new(self).execute(retry_lease: false)
end
# rubocop: enable CodeReuse/ServiceClass
diff --git a/app/models/namespace/aggregation_schedule.rb b/app/models/namespace/aggregation_schedule.rb
index 0bef352cf24..61a7eb4b576 100644
--- a/app/models/namespace/aggregation_schedule.rb
+++ b/app/models/namespace/aggregation_schedule.rb
@@ -6,21 +6,13 @@ class Namespace::AggregationSchedule < ApplicationRecord
self.primary_key = :namespace_id
- DEFAULT_LEASE_TIMEOUT = 3.hours
+ DEFAULT_LEASE_TIMEOUT = 1.5.hours.to_i
REDIS_SHARED_KEY = 'gitlab:update_namespace_statistics_delay'.freeze
belongs_to :namespace
after_create :schedule_root_storage_statistics
- def self.delay_timeout
- redis_timeout = Gitlab::Redis::SharedState.with do |redis|
- redis.get(REDIS_SHARED_KEY)
- end
-
- redis_timeout.nil? ? DEFAULT_LEASE_TIMEOUT : redis_timeout.to_i
- end
-
def schedule_root_storage_statistics
run_after_commit_or_now do
try_obtain_lease do
@@ -28,7 +20,7 @@ class Namespace::AggregationSchedule < ApplicationRecord
.perform_async(namespace_id)
Namespaces::RootStatisticsWorker
- .perform_in(self.class.delay_timeout, namespace_id)
+ .perform_in(DEFAULT_LEASE_TIMEOUT, namespace_id)
end
end
end
@@ -37,7 +29,7 @@ class Namespace::AggregationSchedule < ApplicationRecord
# Used by ExclusiveLeaseGuard
def lease_timeout
- self.class.delay_timeout
+ DEFAULT_LEASE_TIMEOUT
end
# Used by ExclusiveLeaseGuard
diff --git a/app/models/note.rb b/app/models/note.rb
index 3f182c1f099..a12d1eb7243 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -27,6 +27,10 @@ class Note < ApplicationRecord
def values
constants.map {|const| self.const_get(const)}
end
+
+ def value?(val)
+ values.include?(val)
+ end
end
end
diff --git a/app/models/project_statistics.rb b/app/models/project_statistics.rb
index 3802d258664..47999a3694e 100644
--- a/app/models/project_statistics.rb
+++ b/app/models/project_statistics.rb
@@ -93,13 +93,7 @@ class ProjectStatistics < ApplicationRecord
def schedule_namespace_aggregation_worker
run_after_commit do
- next unless schedule_aggregation_worker?
-
Namespaces::ScheduleAggregationWorker.perform_async(project.namespace_id)
end
end
-
- def schedule_aggregation_worker?
- Feature.enabled?(:update_statistics_namespace, project&.root_ancestor)
- end
end
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index 84b1873c05d..52c944491bf 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -68,6 +68,7 @@ class GroupPolicy < BasePolicy
rule { developer }.enable :admin_milestone
rule { reporter }.policy do
+ enable :read_container_image
enable :admin_label
enable :admin_list
enable :admin_issue
diff --git a/app/serializers/analytics_issue_entity.rb b/app/serializers/analytics_issue_entity.rb
index 29d4a6ae1d0..307ce14a921 100644
--- a/app/serializers/analytics_issue_entity.rb
+++ b/app/serializers/analytics_issue_entity.rb
@@ -26,6 +26,6 @@ class AnalyticsIssueEntity < Grape::Entity
private
def url_to(route, object)
- public_send("#{route}_url", object[:path], object[:name], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend
+ public_send("#{route}_url", object[:namespace_path], object[:project_path], object[:iid].to_s) # rubocop:disable GitlabSecurity/PublicSend
end
end
diff --git a/app/serializers/user_serializer.rb b/app/serializers/user_serializer.rb
index 2111e1b5667..d988caea92d 100644
--- a/app/serializers/user_serializer.rb
+++ b/app/serializers/user_serializer.rb
@@ -2,4 +2,21 @@
class UserSerializer < BaseSerializer
entity UserEntity
+
+ def represent(resource, opts = {}, entity = nil)
+ if params[:merge_request_iid]
+ merge_request = opts[:project].merge_requests.find_by_iid!(params[:merge_request_iid])
+ preload_max_member_access(merge_request.project, Array(resource))
+
+ super(resource, opts.merge(merge_request: merge_request), MergeRequestAssigneeEntity)
+ else
+ super
+ end
+ end
+
+ private
+
+ def preload_max_member_access(project, users)
+ project.team.max_member_access_for_user_ids(users.map(&:id))
+ end
end
diff --git a/app/services/application_settings/update_service.rb b/app/services/application_settings/update_service.rb
index 7eeaf8aade1..471df6e2d0c 100644
--- a/app/services/application_settings/update_service.rb
+++ b/app/services/application_settings/update_service.rb
@@ -15,6 +15,8 @@ module ApplicationSettings
update_terms(@params.delete(:terms))
+ add_to_outbound_local_requests_whitelist(@params.delete(:add_to_outbound_local_requests_whitelist))
+
if params.key?(:performance_bar_allowed_group_path)
params[:performance_bar_allowed_group_id] = performance_bar_allowed_group_id
end
@@ -32,6 +34,13 @@ module ApplicationSettings
params.key?(:usage_ping_enabled) || params.key?(:version_check_enabled)
end
+ def add_to_outbound_local_requests_whitelist(values)
+ values_array = Array(values).reject(&:empty?)
+ return if values_array.empty?
+
+ @application_setting.add_to_outbound_local_requests_whitelist(values_array)
+ end
+
def update_terms(terms)
return unless terms.present?
diff --git a/app/services/auth/container_registry_authentication_service.rb b/app/services/auth/container_registry_authentication_service.rb
index 707caee482c..0a069320936 100644
--- a/app/services/auth/container_registry_authentication_service.rb
+++ b/app/services/auth/container_registry_authentication_service.rb
@@ -17,6 +17,14 @@ module Auth
end
def self.full_access_token(*names)
+ access_token(%w(*), names)
+ end
+
+ def self.pull_access_token(*names)
+ access_token(['pull'], names)
+ end
+
+ def self.access_token(actions, names)
names = names.flatten
registry = Gitlab.config.registry
token = JSONWebToken::RSAToken.new(registry.key)
@@ -25,7 +33,7 @@ module Auth
token.expire_time = token_expire_at
token[:access] = names.map do |name|
- { type: 'repository', name: name, actions: %w(*) }
+ { type: 'repository', name: name, actions: actions }
end
token.encoded
diff --git a/app/services/ci/process_pipeline_service.rb b/app/services/ci/process_pipeline_service.rb
index 207cc5017d0..99d4ff9ecd1 100644
--- a/app/services/ci/process_pipeline_service.rb
+++ b/app/services/ci/process_pipeline_service.rb
@@ -4,35 +4,69 @@ module Ci
class ProcessPipelineService < BaseService
attr_reader :pipeline
- def execute(pipeline)
+ def execute(pipeline, trigger_build_ids = nil)
@pipeline = pipeline
update_retried
- new_builds =
- stage_indexes_of_created_processables.flat_map do |index|
- process_stage(index)
- end
+ success = process_stages_without_needs
+
+ # we evaluate dependent needs,
+ # only when the another job has finished
+ success = process_builds_with_needs(trigger_build_ids) || success
@pipeline.update_status
- new_builds.any?
+ success
end
private
- def process_stage(index)
+ def process_stages_without_needs
+ stage_indexes_of_created_processables_without_needs.flat_map do |index|
+ process_stage_without_needs(index)
+ end.any?
+ end
+
+ def process_stage_without_needs(index)
current_status = status_for_prior_stages(index)
- return if HasStatus::BLOCKED_STATUS.include?(current_status)
+ return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
- if HasStatus::COMPLETED_STATUSES.include?(current_status)
- created_processables_in_stage(index).select do |build|
- Gitlab::OptimisticLocking.retry_lock(build) do |subject|
- Ci::ProcessBuildService.new(project, @user)
- .execute(build, current_status)
- end
- end
+ created_processables_in_stage_without_needs(index).select do |build|
+ process_build(build, current_status)
+ end
+ end
+
+ def process_builds_with_needs(trigger_build_ids)
+ return false unless trigger_build_ids.present?
+ return false unless Feature.enabled?(:ci_dag_support, project)
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ trigger_build_names = pipeline.statuses
+ .where(id: trigger_build_ids)
+ .select(:name)
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ created_processables
+ .with_needs(trigger_build_names)
+ .find_each
+ .map(&method(:process_build_with_needs))
+ .any?
+ end
+
+ def process_build_with_needs(build)
+ current_status = status_for_build_needs(build.needs.map(&:name))
+
+ return unless HasStatus::COMPLETED_STATUSES.include?(current_status)
+
+ process_build(build, current_status)
+ end
+
+ def process_build(build, current_status)
+ Gitlab::OptimisticLocking.retry_lock(build) do |subject|
+ Ci::ProcessBuildService.new(project, @user)
+ .execute(subject, current_status)
end
end
@@ -43,17 +77,33 @@ module Ci
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def stage_indexes_of_created_processables
- created_processables.order(:stage_idx).pluck(Arel.sql('DISTINCT stage_idx'))
+ def status_for_build_needs(needs)
+ pipeline.builds.where(name: needs).latest.status || 'success'
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
- def created_processables_in_stage(index)
- created_processables.where(stage_idx: index)
+ def stage_indexes_of_created_processables_without_needs
+ created_processables_without_needs.order(:stage_idx)
+ .pluck(Arel.sql('DISTINCT stage_idx'))
end
# rubocop: enable CodeReuse/ActiveRecord
+ # rubocop: disable CodeReuse/ActiveRecord
+ def created_processables_in_stage_without_needs(index)
+ created_processables_without_needs
+ .where(stage_idx: index)
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+
+ def created_processables_without_needs
+ if Feature.enabled?(:ci_dag_support, project)
+ pipeline.processables.created.without_needs
+ else
+ pipeline.processables.created
+ end
+ end
+
def created_processables
pipeline.processables.created
end
diff --git a/app/services/ci/retry_build_service.rb b/app/services/ci/retry_build_service.rb
index fab8a179843..338495ba030 100644
--- a/app/services/ci/retry_build_service.rb
+++ b/app/services/ci/retry_build_service.rb
@@ -5,7 +5,7 @@ module Ci
CLONE_ACCESSORS = %i[pipeline project ref tag options name
allow_failure stage stage_id stage_idx trigger_request
yaml_variables when environment coverage_regex
- description tag_list protected].freeze
+ description tag_list protected needs].freeze
def execute(build)
reprocess!(build).tap do |new_build|
diff --git a/app/services/merge_requests/mergeability_check_service.rb b/app/services/merge_requests/mergeability_check_service.rb
index 9fa50c9448f..962e2327b3e 100644
--- a/app/services/merge_requests/mergeability_check_service.rb
+++ b/app/services/merge_requests/mergeability_check_service.rb
@@ -3,6 +3,7 @@
module MergeRequests
class MergeabilityCheckService < ::BaseService
include Gitlab::Utils::StrongMemoize
+ include Gitlab::ExclusiveLeaseHelpers
delegate :project, to: :@merge_request
delegate :repository, to: :project
@@ -21,13 +22,35 @@ module MergeRequests
# where we need the current state of the merge ref in repository, the `recheck`
# argument is required.
#
+ # retry_lease - Concurrent calls wait for at least 10 seconds until the
+ # lease is granted (other process finishes running). Returns an error
+ # ServiceResponse if the lease is not granted during this time.
+ #
# Returns a ServiceResponse indicating merge_status is/became can_be_merged
# and the merge-ref is synced. Success in case of being/becoming mergeable,
# error otherwise.
- def execute(recheck: false)
+ def execute(recheck: false, retry_lease: true)
return ServiceResponse.error(message: 'Invalid argument') unless merge_request
return ServiceResponse.error(message: 'Unsupported operation') if Gitlab::Database.read_only?
+ return check_mergeability(recheck) unless merge_ref_auto_sync_lock_enabled?
+
+ in_write_lock(retry_lease: retry_lease) do |retried|
+ # When multiple calls are waiting for the same lock (retry_lease),
+ # it's possible that when granted, the MR status was already updated for
+ # that object, therefore we reset if there was a lease retry.
+ merge_request.reset if retried
+
+ check_mergeability(recheck)
+ end
+ rescue FailedToObtainLockError => error
+ ServiceResponse.error(message: error.message)
+ end
+
+ private
+
+ attr_reader :merge_request
+ def check_mergeability(recheck)
recheck! if recheck
update_merge_status
@@ -46,9 +69,21 @@ module MergeRequests
ServiceResponse.success(payload: payload)
end
- private
+ # It's possible for this service to send concurrent requests to Gitaly in order
+ # to "git update-ref" the same ref. Therefore we handle a light exclusive
+ # lease here.
+ #
+ def in_write_lock(retry_lease:, &block)
+ lease_key = "mergeability_check:#{merge_request.id}"
- attr_reader :merge_request
+ lease_opts = {
+ ttl: 1.minute,
+ retries: retry_lease ? 10 : 0,
+ sleep_sec: retry_lease ? 1.second : 0
+ }
+
+ in_lock(lease_key, lease_opts, &block)
+ end
def payload
strong_memoize(:payload) do
@@ -116,5 +151,9 @@ module MergeRequests
def merge_ref_auto_sync_enabled?
Feature.enabled?(:merge_ref_auto_sync, project, default_enabled: true)
end
+
+ def merge_ref_auto_sync_lock_enabled?
+ Feature.enabled?(:merge_ref_auto_sync_lock, project, default_enabled: true)
+ end
end
end
diff --git a/app/services/self_monitoring/project/create_service.rb b/app/services/self_monitoring/project/create_service.rb
index e5ef8c15456..8ffd22de127 100644
--- a/app/services/self_monitoring/project/create_service.rb
+++ b/app/services/self_monitoring/project/create_service.rb
@@ -14,6 +14,7 @@ module SelfMonitoring
steps :validate_admins,
:create_project,
:add_project_members,
+ :add_to_whitelist,
:add_prometheus_manual_configuration
def initialize
@@ -59,15 +60,29 @@ module SelfMonitoring
end
end
- def add_prometheus_manual_configuration
+ def add_to_whitelist
return success unless prometheus_enabled?
return success unless prometheus_listen_address.present?
- # TODO: Currently, adding the internal prometheus server as a manual configuration
- # is only possible if the setting to allow webhooks and services to connect
- # to local network is on.
- # https://gitlab.com/gitlab-org/gitlab-ce/issues/44496 will add
- # a whitelist that will allow connections to certain ips on the local network.
+ uri = parse_url(internal_prometheus_listen_address_uri)
+ return error(_('Prometheus listen_address is not a valid URI')) unless uri
+
+ result = ApplicationSettings::UpdateService.new(
+ Gitlab::CurrentSettings.current_application_settings,
+ project_owner,
+ outbound_local_requests_whitelist: [uri.normalized_host]
+ ).execute
+
+ if result
+ success
+ else
+ error(_('Could not add prometheus URL to whitelist'))
+ end
+ end
+
+ def add_prometheus_manual_configuration
+ return success unless prometheus_enabled?
+ return success unless prometheus_listen_address.present?
service = project.find_or_initialize_service('prometheus')
@@ -79,6 +94,11 @@ module SelfMonitoring
success
end
+ def parse_url(uri_string)
+ Addressable::URI.parse(uri_string)
+ rescue Addressable::URI::InvalidURIError, TypeError
+ end
+
def prometheus_enabled?
Gitlab.config.prometheus.enable
rescue Settingslogic::MissingSetting
diff --git a/app/services/web_hook_service.rb b/app/services/web_hook_service.rb
index 6d675c026bb..8c294218708 100644
--- a/app/services/web_hook_service.rb
+++ b/app/services/web_hook_service.rb
@@ -17,8 +17,10 @@ class WebHookService
@hook = hook
@data = data
@hook_name = hook_name.to_s
- @request_options = { timeout: Gitlab.config.gitlab.webhook_timeout }
- @request_options.merge!(allow_local_requests: true) if @hook.is_a?(SystemHook)
+ @request_options = {
+ timeout: Gitlab.config.gitlab.webhook_timeout,
+ allow_local_requests: hook.allow_local_requests?
+ }
end
def execute
diff --git a/app/validators/addressable_url_validator.rb b/app/validators/addressable_url_validator.rb
index 273e15ef925..bb445499cee 100644
--- a/app/validators/addressable_url_validator.rb
+++ b/app/validators/addressable_url_validator.rb
@@ -107,6 +107,6 @@ class AddressableUrlValidator < ActiveModel::EachValidator
# calls this validator.
#
# See https://gitlab.com/gitlab-org/gitlab-ee/issues/9833
- ApplicationSetting.current&.allow_local_requests_from_hooks_and_services?
+ ApplicationSetting.current&.allow_local_requests_from_web_hooks_and_services?
end
end
diff --git a/app/validators/system_hook_url_validator.rb b/app/validators/system_hook_url_validator.rb
new file mode 100644
index 00000000000..f4253006dad
--- /dev/null
+++ b/app/validators/system_hook_url_validator.rb
@@ -0,0 +1,18 @@
+# frozen_string_literal: true
+
+# SystemHookUrlValidator
+#
+# Custom validator specific to SystemHook URLs. This validator works like AddressableUrlValidator but
+# it blocks urls pointing to localhost or the local network depending on
+# ApplicationSetting.allow_local_requests_from_system_hooks
+#
+# Example:
+# class SystemHook < WebHook
+# validates :url, system_hook_url: true
+# end
+#
+class SystemHookUrlValidator < PublicUrlValidator
+ def self.allow_setting_local_requests?
+ ApplicationSetting.current&.allow_local_requests_from_system_hooks?
+ end
+end
diff --git a/app/views/admin/application_settings/_outbound.html.haml b/app/views/admin/application_settings/_outbound.html.haml
index 4fecdb59e1d..ad26f52aea7 100644
--- a/app/views/admin/application_settings/_outbound.html.haml
+++ b/app/views/admin/application_settings/_outbound.html.haml
@@ -4,9 +4,13 @@
%fieldset
.form-group
.form-check
- = f.check_box :allow_local_requests_from_hooks_and_services, class: 'form-check-input'
- = f.label :allow_local_requests_from_hooks_and_services, class: 'form-check-label' do
- Allow requests to the local network from hooks and services
+ = f.check_box :allow_local_requests_from_web_hooks_and_services, class: 'form-check-input'
+ = f.label :allow_local_requests_from_web_hooks_and_services, class: 'form-check-label' do
+ = _('Allow requests to the local network from web hooks and services')
+ .form-check
+ = f.check_box :allow_local_requests_from_system_hooks, class: 'form-check-input'
+ = f.label :allow_local_requests_from_system_hooks, class: 'form-check-label' do
+ = _('Allow requests to the local network from system hooks')
.form-group
= f.label :outbound_local_requests_whitelist_raw, class: 'label-bold' do
diff --git a/app/views/clusters/clusters/_banner.html.haml b/app/views/clusters/clusters/_banner.html.haml
index a5de67be96b..4b4278075a6 100644
--- a/app/views/clusters/clusters/_banner.html.haml
+++ b/app/views/clusters/clusters/_banner.html.haml
@@ -3,7 +3,8 @@
%p.js-error-reason
.hidden.js-cluster-creating.bs-callout.bs-callout-info{ role: 'alert' }
- = s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
+ %span.spinner.spinner-dark.spinner-sm{ 'aria-label': 'Loading' }
+ %span.prepend-left-4= s_('ClusterIntegration|Kubernetes cluster is being created on Google Kubernetes Engine...')
.hidden.row.js-cluster-api-unreachable.bs-callout.bs-callout-warning{ role: 'alert' }
.col-11
@@ -18,4 +19,4 @@
%button.js-close-banner.close.cluster-application-banner-close.h-100.m-0= "×"
.hidden.js-cluster-success.bs-callout.bs-callout-success{ role: 'alert' }
- = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine. Refresh the page to see Kubernetes cluster's details")
+ = s_("ClusterIntegration|Kubernetes cluster was successfully created on Google Kubernetes Engine.")
diff --git a/app/views/clusters/clusters/show.html.haml b/app/views/clusters/clusters/show.html.haml
index 4dfbb310142..913d4caa0bc 100644
--- a/app/views/clusters/clusters/show.html.haml
+++ b/app/views/clusters/clusters/show.html.haml
@@ -33,26 +33,29 @@
%section#cluster-integration
%h4= @cluster.name
= render 'banner'
- = render 'form'
-
- = render_if_exists 'projects/clusters/prometheus_graphs'
-
- .cluster-applications-table#js-cluster-applications
-
- %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
- .settings-header
- %h4= s_('ClusterIntegration|Kubernetes cluster details')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
- .settings-content
- = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster)
-
- %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
- .settings-header
- %h4= _('Advanced settings')
- %button.btn.js-settings-toggle{ type: 'button' }
- = expanded ? _('Collapse') : _('Expand')
- %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
- .settings-content#advanced-settings-section
- = render 'advanced_settings'
+
+ - unless @cluster.status_name.in? %i/scheduled creating/
+ = render 'form'
+
+ - unless @cluster.status_name.in? %i/scheduled creating/
+ = render_if_exists 'projects/clusters/prometheus_graphs'
+
+ .cluster-applications-table#js-cluster-applications
+
+ %section.settings#js-cluster-details{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= s_('ClusterIntegration|Kubernetes cluster details')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p= s_('ClusterIntegration|See and edit the details for your Kubernetes cluster')
+ .settings-content
+ = render 'clusters/platforms/kubernetes/form', cluster: @cluster, platform: @cluster.platform_kubernetes, update_cluster_url_path: clusterable.cluster_path(@cluster)
+
+ %section.settings.no-animate#js-cluster-advanced-settings{ class: ('expanded' if expanded) }
+ .settings-header
+ %h4= _('Advanced settings')
+ %button.btn.js-settings-toggle{ type: 'button' }
+ = expanded ? _('Collapse') : _('Expand')
+ %p= s_("ClusterIntegration|Advanced options on this Kubernetes cluster's integration")
+ .settings-content#advanced-settings-section
+ = render 'advanced_settings'
diff --git a/app/views/explore/projects/_projects.html.haml b/app/views/explore/projects/_projects.html.haml
index 67f2f897137..35b32662b8a 100644
--- a/app/views/explore/projects/_projects.html.haml
+++ b/app/views/explore/projects/_projects.html.haml
@@ -1 +1,2 @@
-= render 'shared/projects/list', projects: projects, user: current_user
+- is_explore_page = defined?(explore_page) && explore_page
+= render 'shared/projects/list', projects: projects, user: current_user, explore_page: is_explore_page
diff --git a/app/views/explore/projects/trending.html.haml b/app/views/explore/projects/trending.html.haml
index ed508fa2506..153c90e534e 100644
--- a/app/views/explore/projects/trending.html.haml
+++ b/app/views/explore/projects/trending.html.haml
@@ -10,4 +10,4 @@
= render 'explore/head'
= render 'explore/projects/nav' unless Feature.enabled?(:project_list_filter_bar) && current_user
-= render 'projects', projects: @projects
+= render 'projects', projects: @projects, explore_page: true
diff --git a/app/views/help/_shortcuts.html.haml b/app/views/help/_shortcuts.html.haml
index efb3815b257..46d7c367aa7 100644
--- a/app/views/help/_shortcuts.html.haml
+++ b/app/views/help/_shortcuts.html.haml
@@ -24,11 +24,11 @@
%td.shortcut
%kbd f
%td Focus Filter
- - if performance_bar_enabled?
- %tr
- %td.shortcut
- %kbd p b
- %td Show/hide the Performance Bar
+ %tr
+ %td.shortcut
+ %kbd p
+ %kbd b
+ %td Toggle the Performance Bar
%tr
%td.shortcut
%kbd ?
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 20b844f9fd8..ac774803f95 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -78,4 +78,3 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
= render_if_exists 'layouts/snowplow'
- = render_if_exists 'layouts/pendo' if Feature.enabled?(:pendo_tracking) && !Rails.env.test?
diff --git a/app/views/projects/_flash_messages.html.haml b/app/views/projects/_flash_messages.html.haml
index d95045c9cce..f9222387e97 100644
--- a/app/views/projects/_flash_messages.html.haml
+++ b/app/views/projects/_flash_messages.html.haml
@@ -5,7 +5,7 @@
- if current_user && can?(current_user, :download_code, project)
= render 'shared/no_ssh'
= render 'shared/no_password'
- = render_if_exists 'shared/shared_runners_minutes_limit', project: project
- unless project.empty_repo?
= render 'shared/auto_devops_implicitly_enabled_banner', project: project
= render_if_exists 'projects/above_size_limit_warning', project: project
+ = render_if_exists 'shared/shared_runners_minutes_limit', project: project, classes: [container_class, ("limit-container-width" unless fluid_layout)]
diff --git a/app/views/projects/jobs/show.html.haml b/app/views/projects/jobs/show.html.haml
index c7fab87a593..a3688c17041 100644
--- a/app/views/projects/jobs/show.html.haml
+++ b/app/views/projects/jobs/show.html.haml
@@ -7,9 +7,10 @@
= stylesheet_link_tag 'page_bundles/xterm'
%div{ class: container_class }
- #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json),
+ #js-job-vue-app{ data: { endpoint: project_job_path(@project, @build, format: :json), project_path: @project.full_path,
deployment_help_url: help_page_path('user/project/clusters/index.html', anchor: 'troubleshooting-failed-deployment-jobs'),
runner_help_url: help_page_path('ci/runners/README.html', anchor: 'setting-maximum-job-timeout-for-a-runner'),
runner_settings_url: project_runners_path(@build.project, anchor: 'js-runners-settings'),
variables_settings_url: project_variables_path(@build.project, anchor: 'js-cicd-variables-settings'),
+ page_path: project_job_path(@project, @build), build_status: @build.status, build_stage: @build.stage, log_state: '',
build_options: javascript_build_options } }
diff --git a/app/views/projects/mirrors/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 33e5a6e67c3..1a163cc4a54 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -2,7 +2,7 @@
%ul
%li
= _('The repository must be accessible over <code>http://</code>,
- <code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe
+ <code>https://</code>, <code>ssh://</code> or <code>git://</code>.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
%li
- minutes = Gitlab.config.gitlab_shell.git_timeout / 60
diff --git a/app/views/projects/services/prometheus/_metrics.html.haml b/app/views/projects/services/prometheus/_metrics.html.haml
index a1d74b91002..3aefb3fdbb9 100644
--- a/app/views/projects/services/prometheus/_metrics.html.haml
+++ b/app/views/projects/services/prometheus/_metrics.html.haml
@@ -1,28 +1,34 @@
- project = local_assigns.fetch(:project)
-.card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } }
- .card-header
- = s_('PrometheusService|Common metrics')
- %span.badge.badge-pill.js-monitored-count 0
- .card-body
- .loading-metrics.js-loading-metrics
- %p.prepend-top-10.prepend-left-10
- = icon('spinner spin', class: 'metrics-load-spinner')
- = s_('PrometheusService|Finding and configuring metrics...')
- .empty-metrics.hidden.js-empty-metrics
- %p.text-tertiary.prepend-top-10.prepend-left-10
- = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics')
- %ul.list-unstyled.metrics-list.hidden.js-metrics-list
+.col-lg-3
+ %p
+ = s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer"
-.card.hidden.js-panel-missing-env-vars
- .card-header
- = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
- = s_('PrometheusService|Missing environment variable')
- %span.badge.badge-pill.js-env-var-count 0
- .card-body.hidden
- .flash-container
- .flash-notice
- .flash-text
- = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
- %ul.list-unstyled.metrics-list.js-missing-var-metrics-list
+.col-lg-9
+ .card.js-panel-monitored-metrics{ data: { active_metrics: active_common_project_prometheus_metrics_path(project, :json), metrics_help_path: help_page_path('user/project/integrations/prometheus_library/index') } }
+ .card-header
+ = s_('PrometheusService|Common metrics')
+ %span.badge.badge-pill.js-monitored-count 0
+ .card-body
+ .loading-metrics.js-loading-metrics
+ %p.prepend-top-10.prepend-left-10
+ = icon('spinner spin', class: 'metrics-load-spinner')
+ = s_('PrometheusService|Finding and configuring metrics...')
+ .empty-metrics.hidden.js-empty-metrics
+ %p.text-tertiary.prepend-top-10.prepend-left-10
+ = s_('PrometheusService|Waiting for your first deployment to an environment to find common metrics')
+ %ul.list-unstyled.metrics-list.hidden.js-metrics-list
+
+ .card.hidden.js-panel-missing-env-vars
+ .card-header
+ = icon('caret-right lg fw', class: 'panel-toggle js-panel-toggle', 'aria-label' => 'Toggle panel')
+ = s_('PrometheusService|Missing environment variable')
+ %span.badge.badge-pill.js-env-var-count 0
+ .card-body.hidden
+ .flash-container
+ .flash-notice
+ .flash-text
+ = s_("PrometheusService|To set up automatic monitoring, add the environment variable %{variable} to exporter's queries." % { variable: "<code>$CI_ENVIRONMENT_SLUG</code>" }).html_safe
+ = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus', anchor: 'metrics-and-labels')
+ %ul.list-unstyled.metrics-list.js-missing-var-metrics-list
diff --git a/app/views/projects/services/prometheus/_show.html.haml b/app/views/projects/services/prometheus/_show.html.haml
index 6aafa85e99a..c719661d8e8 100644
--- a/app/views/projects/services/prometheus/_show.html.haml
+++ b/app/views/projects/services/prometheus/_show.html.haml
@@ -1,12 +1,9 @@
-.row.prepend-top-default.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
+.row
.col-lg-3
%h4.prepend-top-0
= s_('PrometheusService|Metrics')
- %p
- = s_('PrometheusService|Common metrics are automatically monitored based on a library of metrics from popular exporters.')
- = link_to s_('PrometheusService|More information'), help_page_path('user/project/integrations/prometheus_library/index'), target: '_blank', rel: "noopener noreferrer"
- .col-lg-9
- = render 'projects/services/prometheus/metrics', project: @project
+.row.append-bottom-default.prometheus-metrics-monitoring.js-prometheus-metrics-monitoring
+ = render 'projects/services/prometheus/metrics', project: @project
= render_if_exists 'projects/services/prometheus/external_alerts', project: @project
diff --git a/app/views/projects/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 2d108a1cba5..498a9744783 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -99,7 +99,7 @@
%code \(\d+.\d+\%\) covered
%li
pytest-cov (Python) -
- %code ^TOTAL\s+\d+\s+\d+\s+(\d+\%)$
+ %code ^TOTAL.+?(\d+\%)$
%li
phpunit --coverage-text --colors=never (PHP) -
%code ^\s*Lines:\s*\d+.\d+\%
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index e253413929a..c9458475aa5 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -1,6 +1,7 @@
- type = local_assigns.fetch(:type)
- board = local_assigns.fetch(:board, nil)
-- block_css_class = type != :boards_modal ? 'row-content-block second-block' : ''
+- is_not_boards_modal_or_productivity_analytics = type != :boards_modal && type != :productivity_analytics
+- block_css_class = is_not_boards_modal_or_productivity_analytics ? 'row-content-block second-block' : ''
- user_can_admin_list = board && can?(current_user, :admin_list, board.parent)
.issues-filters{ class: ("w-100" if type == :boards_modal) }
@@ -155,5 +156,5 @@
- if @project
#js-add-issues-btn.prepend-left-10{ data: { can_admin_list: can?(current_user, :admin_list, @project) } }
#js-toggle-focus-btn
- - elsif type != :boards_modal
+ - elsif is_not_boards_modal_or_productivity_analytics
= render 'shared/issuable/sort_dropdown'
diff --git a/app/views/shared/issuable/_sort_dropdown.html.haml b/app/views/shared/issuable/_sort_dropdown.html.haml
index df0523595f5..8260915c2ab 100644
--- a/app/views/shared/issuable/_sort_dropdown.html.haml
+++ b/app/views/shared/issuable/_sort_dropdown.html.haml
@@ -1,7 +1,7 @@
- sort_value = @sort
- sort_title = issuable_sort_option_title(sort_value)
- viewing_issues = controller.controller_name == 'issues' || controller.action_name == 'issues'
-- manual_sorting = viewing_issues && controller.controller_name != 'dashboard' && Feature.enabled?(:manual_sorting, default_enabled: true)
+- manual_sorting = viewing_issues && controller.controller_name != 'dashboard'
.dropdown.inline.prepend-left-10.issue-sort-dropdown
.btn-group{ role: 'group' }
diff --git a/app/views/shared/milestones/_top.html.haml b/app/views/shared/milestones/_top.html.haml
index 43503e1d08a..fd3317341f6 100644
--- a/app/views/shared/milestones/_top.html.haml
+++ b/app/views/shared/milestones/_top.html.haml
@@ -53,7 +53,7 @@
- close_msg = group ? 'You may close the milestone now.' : 'Navigate to the project to close the milestone.'
%span All issues for this milestone are closed. #{close_msg}
-= render_if_exists 'shared/milestones/burndown', milestone: @milestone, project: @project
+= render_if_exists 'shared/milestones/burndown', milestone: milestone, project: @project
- if is_dynamic_milestone
.table-holder
diff --git a/app/views/shared/notes/_hints.html.haml b/app/views/shared/notes/_hints.html.haml
index 72ede50dd8c..fae7d6526e8 100644
--- a/app/views/shared/notes/_hints.html.haml
+++ b/app/views/shared/notes/_hints.html.haml
@@ -1,13 +1,14 @@
- supports_quick_actions = local_assigns.fetch(:supports_quick_actions, false)
.comment-toolbar.clearfix
.toolbar-text
- - md_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" tabindex="-1">'.html_safe % { url: help_page_path('user/markdown') }
- - actions_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer" tabindex="-1">'.html_safe % { url: help_page_path('user/project/quick_actions') }
- - link_end = '</a>'.html_safe
+ = link_to _('Markdown'), help_page_path('user/markdown'), target: '_blank', tabindex: -1
- if supports_quick_actions
- = s_('Editor|%{mdLinkStart}Markdown%{mdLinkEnd} and %{actionsLinkStart}quick actions%{actionsLinkEnd} are supported').html_safe % { mdLinkStart: md_link_start, mdLinkEnd: link_end, actionsLinkStart: actions_link_start, actionsLinkEnd: link_end }
+ and
+ = link_to _('quick actions'), help_page_path('user/project/quick_actions'), target: '_blank', tabindex: -1
+ are
- else
- = s_('Editor|%{mdLinkStart}Markdown is supported%{mdLinkEnd}').html_safe % { mdLinkStart: md_link_start, mdLinkEnd: link_end }
+ is
+ supported
%span.uploading-container
%span.uploading-progress-container.hide
diff --git a/app/views/shared/projects/_list.html.haml b/app/views/shared/projects/_list.html.haml
index 576ec3e1782..67cb1aa549c 100644
--- a/app/views/shared/projects/_list.html.haml
+++ b/app/views/shared/projects/_list.html.haml
@@ -21,6 +21,7 @@
- own_projects_current_user_empty_message_header = s_('UserProfile|You haven\'t created any personal projects.')
- own_projects_current_user_empty_message_description = s_('UserProfile|Your projects can be available publicly, internally, or privately, at your choice.')
- own_projects_visitor_empty_message = s_('UserProfile|This user doesn\'t have any personal projects')
+- explore_page_empty_message = s_('UserProfile|Explore public groups to find projects to contribute to.')
- primary_button_label = _('New project')
- primary_button_link = new_project_path
- secondary_button_label = _('Explore groups')
@@ -58,4 +59,4 @@
current_user_empty_message_description: own_projects_current_user_empty_message_description,
primary_button_label: primary_button_label,
primary_button_link: primary_button_link,
- visitor_empty_message: own_projects_visitor_empty_message }
+ visitor_empty_message: defined?(explore_page) && explore_page ? explore_page_empty_message : own_projects_visitor_empty_message }
diff --git a/app/views/u2f/_register.html.haml b/app/views/u2f/_register.html.haml
index f6724f72307..ef3835332a7 100644
--- a/app/views/u2f/_register.html.haml
+++ b/app/views/u2f/_register.html.haml
@@ -16,7 +16,7 @@
.col-md-4
%button#js-setup-u2f-device.btn.btn-info.btn-block{ disabled: true }= _("Set up new U2F device")
.col-md-8
- %p.text-warning= _("You need to register a two-factor authentication app before you can set up a U2F device.")
+ %p= _("You need to register a two-factor authentication app before you can set up a U2F device.")
%script#js-register-u2f-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.")
diff --git a/app/workers/all_queues.yml b/app/workers/all_queues.yml
index 991a177018e..400becdd023 100644
--- a/app/workers/all_queues.yml
+++ b/app/workers/all_queues.yml
@@ -88,6 +88,7 @@
- pipeline_processing:ci_build_prepare
- pipeline_processing:build_queue
- pipeline_processing:build_success
+- pipeline_processing:build_process
- pipeline_processing:pipeline_process
- pipeline_processing:pipeline_success
- pipeline_processing:pipeline_update
diff --git a/app/workers/build_process_worker.rb b/app/workers/build_process_worker.rb
new file mode 100644
index 00000000000..9cd9519df1f
--- /dev/null
+++ b/app/workers/build_process_worker.rb
@@ -0,0 +1,16 @@
+# frozen_string_literal: true
+
+class BuildProcessWorker
+ include ApplicationWorker
+ include PipelineQueue
+
+ queue_namespace :pipeline_processing
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def perform(build_id)
+ CommitStatus.find_by(id: build_id).try do |build|
+ build.pipeline.process!([build_id])
+ end
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
+end
diff --git a/app/workers/namespaces/root_statistics_worker.rb b/app/workers/namespaces/root_statistics_worker.rb
index 48876825564..0c1ca5eb975 100644
--- a/app/workers/namespaces/root_statistics_worker.rb
+++ b/app/workers/namespaces/root_statistics_worker.rb
@@ -9,7 +9,7 @@ module Namespaces
def perform(namespace_id)
namespace = Namespace.find(namespace_id)
- return unless update_statistics_enabled_for?(namespace) && namespace.aggregation_scheduled?
+ return unless namespace.aggregation_scheduled?
Namespaces::StatisticsRefresherService.new.execute(namespace)
@@ -23,9 +23,5 @@ module Namespaces
def log_error(namespace_path, error_message)
Gitlab::SidekiqLogger.error("Namespace statistics can't be updated for #{namespace_path}: #{error_message}")
end
-
- def update_statistics_enabled_for?(namespace)
- Feature.enabled?(:update_statistics_namespace, namespace)
- end
end
end
diff --git a/app/workers/namespaces/schedule_aggregation_worker.rb b/app/workers/namespaces/schedule_aggregation_worker.rb
index a4594b84b13..983ce4bef4a 100644
--- a/app/workers/namespaces/schedule_aggregation_worker.rb
+++ b/app/workers/namespaces/schedule_aggregation_worker.rb
@@ -12,7 +12,7 @@ module Namespaces
namespace = Namespace.find(namespace_id)
root_ancestor = namespace.root_ancestor
- return unless update_statistics_enabled_for?(root_ancestor) && !root_ancestor.aggregation_scheduled?
+ return if root_ancestor.aggregation_scheduled?
Namespace::AggregationSchedule.safe_find_or_create_by!(namespace_id: root_ancestor.id)
rescue ActiveRecord::RecordNotFound
@@ -37,9 +37,5 @@ module Namespaces
def log_error(root_ancestor_id)
Gitlab::SidekiqLogger.error("Namespace can't be scheduled for aggregation: #{root_ancestor_id} does not exist")
end
-
- def update_statistics_enabled_for?(root_ancestor)
- Feature.enabled?(:update_statistics_namespace, root_ancestor)
- end
end
end
diff --git a/app/workers/pipeline_process_worker.rb b/app/workers/pipeline_process_worker.rb
index f2aa17acb51..96524d93f8d 100644
--- a/app/workers/pipeline_process_worker.rb
+++ b/app/workers/pipeline_process_worker.rb
@@ -7,9 +7,10 @@ class PipelineProcessWorker
queue_namespace :pipeline_processing
# rubocop: disable CodeReuse/ActiveRecord
- def perform(pipeline_id)
- Ci::Pipeline.find_by(id: pipeline_id)
- .try(:process!)
+ def perform(pipeline_id, build_ids = nil)
+ Ci::Pipeline.find_by(id: pipeline_id).try do |pipeline|
+ pipeline.process!(build_ids)
+ end
end
# rubocop: enable CodeReuse/ActiveRecord
end