summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock7
-rw-r--r--app/assets/javascripts/monitoring/components/dashboard.vue165
-rw-r--r--app/assets/javascripts/notes/components/note_body.vue6
-rw-r--r--app/assets/javascripts/notes/stores/actions.js48
-rw-r--r--app/assets/javascripts/notes/stores/utils.js7
-rw-r--r--app/assets/javascripts/operation_settings/components/external_dashboard.vue57
-rw-r--r--app/assets/javascripts/operation_settings/index.js26
-rw-r--r--app/assets/javascripts/pages/profiles/keys/index.js2
-rw-r--r--app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js42
-rw-r--r--app/assets/javascripts/pages/projects/settings/operations/show/index.js2
-rw-r--r--app/assets/javascripts/profile/profile.js11
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js4
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_create.js3
-rw-r--r--app/assets/javascripts/protected_branches/protected_branch_edit.js3
-rw-r--r--app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue4
-rw-r--r--app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue4
-rw-r--r--app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue20
-rw-r--r--app/assets/stylesheets/framework/common.scss24
-rw-r--r--app/assets/stylesheets/framework/header.scss7
-rw-r--r--app/assets/stylesheets/framework/modal.scss6
-rw-r--r--app/assets/stylesheets/framework/typography.scss4
-rw-r--r--app/assets/stylesheets/framework/variables.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss5
-rw-r--r--app/controllers/omniauth_callbacks_controller.rb7
-rw-r--r--app/controllers/profiles/preferences_controller.rb4
-rw-r--r--app/controllers/profiles_controller.rb1
-rw-r--r--app/controllers/projects/environments_controller.rb1
-rw-r--r--app/controllers/projects/settings/operations_controller.rb4
-rw-r--r--app/finders/issuable_finder.rb26
-rw-r--r--app/finders/issues_finder.rb4
-rw-r--r--app/finders/projects_finder.rb8
-rw-r--r--app/models/concerns/has_status.rb4
-rw-r--r--app/models/project.rb28
-rw-r--r--app/models/remote_mirror.rb4
-rw-r--r--app/models/user.rb11
-rw-r--r--app/models/user_preference.rb4
-rw-r--r--app/policies/group_policy.rb6
-rw-r--r--app/uploaders/import_export_uploader.rb4
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml1
-rw-r--r--app/views/profiles/preferences/show.html.haml26
-rw-r--r--app/views/profiles/show.html.haml12
-rw-r--r--app/views/projects/issues/show.html.haml2
-rw-r--r--app/views/projects/mirrors/_disabled_mirror_badge.html.haml1
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml30
-rw-r--r--app/views/projects/settings/operations/_external_dashboard.html.haml2
-rw-r--r--app/views/projects/settings/operations/show.html.haml1
-rw-r--r--app/views/shared/_remote_mirror_update_button.html.haml2
-rw-r--r--changelogs/unreleased/10808-allow-license-import-during-install.yml5
-rw-r--r--changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml5
-rw-r--r--changelogs/unreleased/53973-fix-subpixel-border-issue.yml5
-rw-r--r--changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml5
-rw-r--r--changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml5
-rw-r--r--changelogs/unreleased/57077-add-salesforce-omniauth.yml5
-rw-r--r--changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml5
-rw-r--r--changelogs/unreleased/60462-empty-pipeline-section.yml5
-rw-r--r--changelogs/unreleased/61494-set-status-modal-visual-bugs.yml5
-rw-r--r--changelogs/unreleased/ce-11430-update_clair_local_scan.yml5
-rw-r--r--changelogs/unreleased/fix-js-error-ssh-key-view.yml5
-rw-r--r--changelogs/unreleased/fj-59522-improve-search-controller-performance.yml5
-rw-r--r--changelogs/unreleased/friendly-wrap-component.yml5
-rw-r--r--changelogs/unreleased/gitaly-version-v1.42.0.yml5
-rw-r--r--changelogs/unreleased/sh-cleanup-import-export.yml5
-rw-r--r--changelogs/unreleased/sh-fix-related-merge-requests-path.yml5
-rw-r--r--changelogs/unreleased/shell-9-1-0.yml5
-rw-r--r--changelogs/unreleased/show-disabled-mirrors.yml5
-rw-r--r--changelogs/unreleased/winh-notes-error-handling.yml5
-rw-r--r--config/gitlab.yml.example4
-rw-r--r--db/migrate/20190325105715_add_fields_to_user_preferences.rb24
-rw-r--r--db/schema.rb3
-rw-r--r--doc/administration/auth/ldap.md4
-rw-r--r--doc/administration/high_availability/README.md26
-rw-r--r--doc/administration/index.md1
-rw-r--r--doc/administration/monitoring/prometheus/gitlab_metrics.md13
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/development/architecture.md148
-rw-r--r--doc/install/installation.md6
-rw-r--r--doc/install/requirements.md4
-rw-r--r--doc/integration/img/salesforce_app_details.pngbin0 -> 116022 bytes
-rw-r--r--doc/integration/img/salesforce_app_secret_details.pngbin0 -> 199643 bytes
-rw-r--r--doc/integration/img/salesforce_oauth_app_details.pngbin0 -> 171542 bytes
-rw-r--r--doc/integration/omniauth.md1
-rw-r--r--doc/integration/salesforce.md79
-rw-r--r--doc/update/mysql_to_postgresql.md16
-rw-r--r--doc/user/admin_area/license.md33
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md2
-rw-r--r--doc/user/discussions/index.md14
-rw-r--r--lib/api/helpers/related_resources_helpers.rb4
-rw-r--r--lib/api/merge_requests.rb4
-rw-r--r--lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml2
-rw-r--r--lib/gitlab/database/migration_helpers.rb6
-rw-r--r--lib/gitlab/gitaly_client.rb3
-rw-r--r--lib/gitlab/group_search_results.rb6
-rw-r--r--lib/gitlab/metrics/samplers/ruby_sampler.rb31
-rw-r--r--lib/gitlab/metrics/samplers/unicorn_sampler.rb32
-rw-r--r--lib/gitlab/metrics/system.rb23
-rw-r--r--lib/gitlab/project_search_results.rb4
-rw-r--r--lib/gitlab/search_results.rb88
-rw-r--r--locale/gitlab.pot63
-rw-r--r--qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb3
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb3
-rw-r--r--spec/factories/projects.rb1
-rw-r--r--spec/features/dashboard/user_filters_projects_spec.rb10
-rw-r--r--spec/features/merge_request/user_sees_merge_widget_spec.rb22
-rw-r--r--spec/features/merge_request/user_suggests_changes_on_diff_spec.rb22
-rw-r--r--spec/features/oauth_login_spec.rb2
-rw-r--r--spec/features/profiles/user_edit_preferences_spec.rb32
-rw-r--r--spec/features/profiles/user_edit_profile_spec.rb32
-rw-r--r--spec/features/projects/settings/repository_settings_spec.rb12
-rw-r--r--spec/finders/issues_finder_spec.rb8
-rw-r--r--spec/finders/merge_requests_finder_spec.rb2
-rw-r--r--spec/frontend/notes/stores/utils_spec.js17
-rw-r--r--spec/frontend/operation_settings/components/external_dashboard_spec.js100
-rw-r--r--spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js103
-rw-r--r--spec/javascripts/monitoring/dashboard_spec.js69
-rw-r--r--spec/javascripts/notes/stores/actions_spec.js175
-rw-r--r--spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js77
-rw-r--r--spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js13
-rw-r--r--spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js75
-rw-r--r--spec/lib/api/helpers/related_resources_helpers_spec.rb34
-rw-r--r--spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb30
-rw-r--r--spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb25
-rw-r--r--spec/lib/gitlab/metrics/system_spec.rb24
-rw-r--r--spec/models/project_spec.rb104
-rw-r--r--spec/models/remote_mirror_spec.rb16
-rw-r--r--spec/models/user_preference_spec.rb6
-rw-r--r--spec/requests/api/merge_requests_spec.rb27
-rw-r--r--spec/requests/api/projects_spec.rb3
-rw-r--r--spec/services/users/update_service_spec.rb9
-rw-r--r--spec/support/capybara.rb11
-rw-r--r--spec/support/helpers/features/notes_helpers.rb12
-rw-r--r--spec/support/helpers/mobile_helpers.rb2
-rw-r--r--spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb7
-rw-r--r--spec/uploaders/import_export_uploader_spec.rb32
136 files changed, 2110 insertions, 417 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index 7d47e599800..a50908ca3da 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-1.41.0
+1.42.0
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index f7ee06693c1..47da986f86f 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-9.0.0
+9.1.0
diff --git a/Gemfile b/Gemfile
index 1282ff0e20d..19432758b34 100644
--- a/Gemfile
+++ b/Gemfile
@@ -43,6 +43,7 @@ gem 'omniauth_crowd', '~> 2.2.0'
gem 'omniauth-authentiq', '~> 0.3.3'
gem 'omniauth_openid_connect', '~> 0.3.0'
gem "omniauth-ultraauth", '~> 0.0.2'
+gem 'omniauth-salesforce', '~> 1.0.5'
gem 'rack-oauth2', '~> 1.9.3'
gem 'jwt', '~> 2.1.0'
@@ -406,6 +407,7 @@ gem 'health_check', '~> 2.6.0'
# System information
gem 'vmstat', '~> 2.3.0'
gem 'sys-filesystem', '~> 1.1.6'
+gem 'sys-proctable', '~> 1.2'
# SSH host key support
gem 'net-ssh', '~> 5.0'
diff --git a/Gemfile.lock b/Gemfile.lock
index 9b1a036030a..3dd1adb5f63 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -553,6 +553,9 @@ GEM
omniauth (~> 1.9)
omniauth-oauth2-generic (0.2.2)
omniauth-oauth2 (~> 1.0)
+ omniauth-salesforce (1.0.5)
+ omniauth (~> 1.0)
+ omniauth-oauth2 (~> 1.0)
omniauth-saml (1.10.0)
omniauth (~> 1.3, >= 1.3.2)
ruby-saml (~> 1.7)
@@ -901,6 +904,8 @@ GEM
httpclient (>= 2.4)
sys-filesystem (1.1.6)
ffi
+ sys-proctable (1.2.1)
+ ffi
sysexits (1.2.0)
temple (0.8.0)
test-prof (0.2.5)
@@ -1127,6 +1132,7 @@ DEPENDENCIES
omniauth-google-oauth2 (~> 0.6.0)
omniauth-kerberos (~> 0.3.0)
omniauth-oauth2-generic (~> 0.2.2)
+ omniauth-salesforce (~> 1.0.5)
omniauth-saml (~> 1.10)
omniauth-shibboleth (~> 1.3.0)
omniauth-twitter (~> 1.4)
@@ -1205,6 +1211,7 @@ DEPENDENCIES
stackprof (~> 0.2.10)
state_machines-activerecord (~> 0.5.1)
sys-filesystem (~> 1.1.6)
+ sys-proctable (~> 1.2)
test-prof (~> 0.2.5)
thin (~> 1.7.0)
timecop (~> 0.8.0)
diff --git a/app/assets/javascripts/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 33f6afc9c2d..ff1e1805948 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -1,5 +1,12 @@
<script>
-import { GlDropdown, GlDropdownItem, GlLink } from '@gitlab/ui';
+import {
+ GlButton,
+ GlDropdown,
+ GlDropdownItem,
+ GlModal,
+ GlModalDirective,
+ GlLink,
+} from '@gitlab/ui';
import _ from 'underscore';
import { s__ } from '~/locale';
import Icon from '~/vue_shared/components/icon.vue';
@@ -23,12 +30,21 @@ export default {
GraphGroup,
EmptyState,
Icon,
+ GlButton,
GlDropdown,
GlDropdownItem,
GlLink,
+ GlModal,
+ },
+ directives: {
+ GlModalDirective,
},
-
props: {
+ externalDashboardPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
hasMetrics: {
type: Boolean,
required: false,
@@ -96,6 +112,19 @@ export default {
type: Boolean,
required: true,
},
+ customMetricsAvailable: {
+ type: Boolean,
+ required: false,
+ default: false,
+ },
+ customMetricsPath: {
+ type: String,
+ required: true,
+ },
+ validateQueryPath: {
+ type: String,
+ required: true,
+ },
},
data() {
return {
@@ -105,8 +134,14 @@ export default {
elWidth: 0,
selectedTimeWindow: '',
selectedTimeWindowKey: '',
+ formIsValid: null,
};
},
+ computed: {
+ canAddMetrics() {
+ return this.customMetricsAvailable && this.customMetricsPath.length;
+ },
+ },
created() {
this.service = new MonitoringService({
metricsEndpoint: this.metricsEndpoint,
@@ -187,11 +222,20 @@ export default {
this.state = 'unableToConnect';
});
},
+ hideAddMetricModal() {
+ this.$refs.addMetricModal.hide();
+ },
onSidebarMutation() {
setTimeout(() => {
this.elWidth = this.$el.clientWidth;
}, sidebarAnimationDuration);
},
+ setFormValidity(isValid) {
+ this.formIsValid = isValid;
+ },
+ submitCustomMetricsForm() {
+ this.$refs.customMetricsForm.submit();
+ },
activeTimeWindow(key) {
return this.timeWindows[key] === this.selectedTimeWindow;
},
@@ -199,47 +243,96 @@ export default {
return `?time_window=${key}`;
},
},
+ addMetric: {
+ title: s__('Metrics|Add metric'),
+ modalId: 'add-metric',
+ },
};
</script>
<template>
- <div v-if="!showEmptyState" class="prometheus-graphs prepend-top-default">
- <div
- v-if="environmentsEndpoint"
- class="dropdowns d-flex align-items-center justify-content-between"
- >
- <div class="d-flex align-items-center">
- <strong>{{ s__('Metrics|Environment') }}</strong>
- <gl-dropdown
- class="prepend-left-10 js-environments-dropdown"
- toggle-class="dropdown-menu-toggle"
- :text="currentEnvironmentName"
- :disabled="store.environmentsData.length === 0"
- >
- <gl-dropdown-item
- v-for="environment in store.environmentsData"
- :key="environment.id"
- :href="environment.metrics_path"
- :active="environment.name === currentEnvironmentName"
- active-class="is-active"
- >{{ environment.name }}</gl-dropdown-item
+ <div v-if="!showEmptyState" class="prometheus-graphs">
+ <div class="gl-p-3 border-bottom bg-gray-light d-flex justify-content-between">
+ <div
+ v-if="environmentsEndpoint"
+ class="dropdowns d-flex align-items-center justify-content-between"
+ >
+ <div class="d-flex align-items-center">
+ <strong>{{ s__('Metrics|Environment') }}</strong>
+ <gl-dropdown
+ class="prepend-left-10 js-environments-dropdown"
+ toggle-class="dropdown-menu-toggle"
+ :text="currentEnvironmentName"
+ :disabled="store.environmentsData.length === 0"
+ >
+ <gl-dropdown-item
+ v-for="environment in store.environmentsData"
+ :key="environment.id"
+ :active="environment.name === currentEnvironmentName"
+ active-class="is-active"
+ >{{ environment.name }}</gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
+ <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
+ <strong>{{ s__('Metrics|Show last') }}</strong>
+ <gl-dropdown
+ class="prepend-left-10 js-time-window-dropdown"
+ toggle-class="dropdown-menu-toggle"
+ :text="selectedTimeWindow"
>
- </gl-dropdown>
+ <gl-dropdown-item
+ v-for="(value, key) in timeWindows"
+ :key="key"
+ :active="activeTimeWindow(key)"
+ ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
+ >
+ </gl-dropdown>
+ </div>
</div>
- <div v-if="showTimeWindowDropdown" class="d-flex align-items-center">
- <strong>{{ s__('Metrics|Show last') }}</strong>
- <gl-dropdown
- class="prepend-left-10 js-time-window-dropdown"
- toggle-class="dropdown-menu-toggle"
- :text="selectedTimeWindow"
- >
- <gl-dropdown-item
- v-for="(value, key) in timeWindows"
- :key="key"
- :active="activeTimeWindow(key)"
- ><gl-link :href="setTimeWindowParameter(key)">{{ value }}</gl-link></gl-dropdown-item
+ <div class="d-flex">
+ <div v-if="isEE && canAddMetrics">
+ <gl-button
+ v-gl-modal-directive="$options.addMetric.modalId"
+ class="js-add-metric-button text-success border-success"
+ >
+ {{ $options.addMetric.title }}
+ </gl-button>
+ <gl-modal
+ ref="addMetricModal"
+ :modal-id="$options.addMetric.modalId"
+ :title="$options.addMetric.title"
>
- </gl-dropdown>
+ <form ref="customMetricsForm" :action="customMetricsPath" method="post">
+ <custom-metrics-form-fields
+ :validate-query-path="validateQueryPath"
+ form-operation="post"
+ @formValidation="setFormValidity"
+ />
+ </form>
+ <div slot="modal-footer">
+ <gl-button @click="hideAddMetricModal">
+ {{ __('Cancel') }}
+ </gl-button>
+ <gl-button
+ :disabled="!formIsValid"
+ variant="success"
+ @click="submitCustomMetricsForm"
+ >
+ {{ __('Save changes') }}
+ </gl-button>
+ </div>
+ </gl-modal>
+ </div>
+ <gl-button
+ v-if="externalDashboardPath.length"
+ class="js-external-dashboard-link prepend-left-8"
+ variant="primary"
+ :href="externalDashboardPath"
+ >
+ {{ __('View full dashboard') }}
+ <icon name="external-link" />
+ </gl-button>
</div>
</div>
<graph-group
diff --git a/app/assets/javascripts/notes/components/note_body.vue b/app/assets/javascripts/notes/components/note_body.vue
index 8ddd5b8514a..88454c3fb4c 100644
--- a/app/assets/javascripts/notes/components/note_body.vue
+++ b/app/assets/javascripts/notes/components/note_body.vue
@@ -83,10 +83,12 @@ export default {
formCancelHandler(shouldConfirm, isDirty) {
this.$emit('cancelForm', shouldConfirm, isDirty);
},
- applySuggestion({ suggestionId, flashContainer, callback }) {
+ applySuggestion({ suggestionId, flashContainer, callback = () => {} }) {
const { discussion_id: discussionId, id: noteId } = this.note;
- this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer, callback });
+ return this.submitSuggestion({ discussionId, noteId, suggestionId, flashContainer }).then(
+ callback,
+ );
},
},
};
diff --git a/app/assets/javascripts/notes/stores/actions.js b/app/assets/javascripts/notes/stores/actions.js
index 970e6551092..63658d49a05 100644
--- a/app/assets/javascripts/notes/stores/actions.js
+++ b/app/assets/javascripts/notes/stores/actions.js
@@ -142,6 +142,23 @@ export const createNewNote = ({ commit, dispatch }, { endpoint, data }) =>
export const removePlaceholderNotes = ({ commit }) => commit(types.REMOVE_PLACEHOLDER_NOTES);
+export const resolveDiscussion = ({ state, dispatch, getters }, { discussionId }) => {
+ const discussion = utils.findNoteObjectById(state.discussions, discussionId);
+ const isResolved = getters.isDiscussionResolved(discussionId);
+
+ if (!discussion) {
+ return Promise.reject();
+ } else if (isResolved) {
+ return Promise.resolve();
+ }
+
+ return dispatch('toggleResolveNote', {
+ endpoint: discussion.resolve_path,
+ isResolved,
+ discussion: true,
+ });
+};
+
export const toggleResolveNote = ({ commit, dispatch }, { endpoint, isResolved, discussion }) =>
service
.toggleResolveNote(endpoint, isResolved)
@@ -251,11 +268,20 @@ export const saveNote = ({ commit, dispatch }, noteData) => {
const { errors } = res;
const commandsChanges = res.commands_changes;
- if (hasQuickActions && errors && Object.keys(errors).length) {
- eTagPoll.makeRequest();
+ if (errors && Object.keys(errors).length) {
+ /*
+ The following reply means that quick actions have been successfully applied:
+
+ {"commands_changes":{},"valid":false,"errors":{"commands_only":["Commands applied"]}}
+ */
+ if (hasQuickActions) {
+ eTagPoll.makeRequest();
- $('.js-gfm-input').trigger('clear-commands-cache.atwho');
- Flash(__('Commands applied'), 'notice', noteData.flashContainer);
+ $('.js-gfm-input').trigger('clear-commands-cache.atwho');
+ Flash(__('Commands applied'), 'notice', noteData.flashContainer);
+ } else {
+ throw new Error(__('Failed to save comment!'));
+ }
}
if (commandsChanges) {
@@ -420,15 +446,13 @@ export const updateResolvableDiscussonsCounts = ({ commit }) =>
commit(types.UPDATE_RESOLVABLE_DISCUSSIONS_COUNTS);
export const submitSuggestion = (
- { commit },
- { discussionId, noteId, suggestionId, flashContainer, callback },
-) => {
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+) =>
service
.applySuggestion(suggestionId)
- .then(() => {
- commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId });
- callback();
- })
+ .then(() => commit(types.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }))
+ .then(() => dispatch('resolveDiscussion', { discussionId }).catch(() => {}))
.catch(err => {
const defaultMessage = __(
'Something went wrong while applying the suggestion. Please try again.',
@@ -436,9 +460,7 @@ export const submitSuggestion = (
const flashMessage = err.response.data ? `${err.response.data.message}.` : defaultMessage;
Flash(__(flashMessage), 'alert', flashContainer);
- callback();
});
-};
export const convertToDiscussion = ({ commit }, noteId) =>
commit(types.CONVERT_TO_DISCUSSION, noteId);
diff --git a/app/assets/javascripts/notes/stores/utils.js b/app/assets/javascripts/notes/stores/utils.js
index 029fde348fb..ed4cef4a917 100644
--- a/app/assets/javascripts/notes/stores/utils.js
+++ b/app/assets/javascripts/notes/stores/utils.js
@@ -2,7 +2,8 @@ import AjaxCache from '~/lib/utils/ajax_cache';
import { trimFirstCharOfLineContent } from '~/diffs/store/utils';
import { sprintf, __ } from '~/locale';
-const REGEX_QUICK_ACTIONS = /^\/\w+.*$/gm;
+// factory function because global flag makes RegExp stateful
+const createQuickActionsRegex = () => /^\/\w+.*$/gm;
export const findNoteObjectById = (notes, id) => notes.filter(n => n.id === id)[0];
@@ -27,9 +28,9 @@ export const getQuickActionText = note => {
return text;
};
-export const hasQuickActions = note => REGEX_QUICK_ACTIONS.test(note);
+export const hasQuickActions = note => createQuickActionsRegex().test(note);
-export const stripQuickActions = note => note.replace(REGEX_QUICK_ACTIONS, '').trim();
+export const stripQuickActions = note => note.replace(createQuickActionsRegex(), '').trim();
export const prepareDiffLines = diffLines =>
diffLines.map(line => ({ ...trimFirstCharOfLineContent(line) }));
diff --git a/app/assets/javascripts/operation_settings/components/external_dashboard.vue b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
new file mode 100644
index 00000000000..0a87d193b72
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/components/external_dashboard.vue
@@ -0,0 +1,57 @@
+<script>
+import { GlButton, GlFormGroup, GlFormInput, GlLink } from '@gitlab/ui';
+
+export default {
+ components: {
+ GlButton,
+ GlFormGroup,
+ GlFormInput,
+ GlLink,
+ },
+ props: {
+ externalDashboardPath: {
+ type: String,
+ required: false,
+ default: '',
+ },
+ externalDashboardHelpPagePath: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+
+<template>
+ <section class="settings expanded">
+ <div class="settings-header">
+ <h4 class="js-section-header">
+ {{ s__('ExternalMetrics|External Dashboard') }}
+ </h4>
+ <p class="js-section-sub-header">
+ {{
+ s__(
+ 'ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards.',
+ )
+ }}
+ <gl-link :href="externalDashboardHelpPagePath">{{ __('Learn more') }}</gl-link>
+ </p>
+ </div>
+ <div class="settings-content">
+ <form>
+ <gl-form-group
+ :label="s__('ExternalMetrics|Full dashboard URL')"
+ :description="s__('ExternalMetrics|Enter the URL of the dashboard you want to link to')"
+ >
+ <gl-form-input
+ :value="externalDashboardPath"
+ placeholder="https://my-org.gitlab.io/my-dashboards"
+ />
+ </gl-form-group>
+ <gl-button variant="success">
+ {{ __('Save Changes') }}
+ </gl-button>
+ </form>
+ </div>
+ </section>
+</template>
diff --git a/app/assets/javascripts/operation_settings/index.js b/app/assets/javascripts/operation_settings/index.js
new file mode 100644
index 00000000000..1171f3ece9f
--- /dev/null
+++ b/app/assets/javascripts/operation_settings/index.js
@@ -0,0 +1,26 @@
+import Vue from 'vue';
+import ExternalDashboardForm from './components/external_dashboard.vue';
+
+export default () => {
+ /**
+ * This check can be removed when we remove
+ * the :grafana_dashboard_link feature flag
+ */
+ if (!gon.features.grafanaDashboardLink) {
+ return null;
+ }
+
+ const el = document.querySelector('.js-operation-settings');
+
+ return new Vue({
+ el,
+ render(createElement) {
+ return createElement(ExternalDashboardForm, {
+ props: {
+ ...el.dataset,
+ expanded: false,
+ },
+ });
+ },
+ });
+};
diff --git a/app/assets/javascripts/pages/profiles/keys/index.js b/app/assets/javascripts/pages/profiles/keys/index.js
index 1cd3ee1dfdb..d3dcd21f456 100644
--- a/app/assets/javascripts/pages/profiles/keys/index.js
+++ b/app/assets/javascripts/pages/profiles/keys/index.js
@@ -2,6 +2,8 @@ import AddSshKeyValidation from '~/profile/add_ssh_key_validation';
document.addEventListener('DOMContentLoaded', () => {
const input = document.querySelector('.js-add-ssh-key-validation-input');
+ if (!input) return;
+
const warning = document.querySelector('.js-add-ssh-key-validation-warning');
const originalSubmit = input.form.querySelector('.js-add-ssh-key-validation-original-submit');
const confirmSubmit = warning.querySelector('.js-add-ssh-key-validation-confirm-submit');
diff --git a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
index c1f6edf2f27..a20a0526f12 100644
--- a/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
+++ b/app/assets/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown.js
@@ -1,4 +1,10 @@
-const defaultTimezone = 'UTC';
+const defaultTimezone = { name: 'UTC', offset: 0 };
+const defaults = {
+ $inputEl: null,
+ $dropdownEl: null,
+ onSelectTimezone: null,
+ displayFormat: item => item.name,
+};
export const formatUtcOffset = offset => {
const parsed = parseInt(offset, 10);
@@ -11,23 +17,28 @@ export const formatUtcOffset = offset => {
export const formatTimezone = item => `[UTC ${formatUtcOffset(item.offset)}] ${item.name}`;
-const defaults = {
- $inputEl: null,
- $dropdownEl: null,
- onSelectTimezone: null,
+export const findTimezoneByIdentifier = (tzList = [], identifier = null) => {
+ if (tzList && tzList.length && identifier && identifier.length) {
+ return tzList.find(tz => tz.identifier === identifier) || null;
+ }
+ return null;
};
export default class TimezoneDropdown {
- constructor({ $dropdownEl, $inputEl, onSelectTimezone } = defaults) {
+ constructor({ $dropdownEl, $inputEl, onSelectTimezone, displayFormat } = defaults) {
this.$dropdown = $dropdownEl;
this.$dropdownToggle = this.$dropdown.find('.dropdown-toggle-text');
this.$input = $inputEl;
this.timezoneData = this.$dropdown.data('data');
+ this.onSelectTimezone = onSelectTimezone;
+ this.displayFormat = displayFormat || defaults.displayFormat;
+
+ this.initialTimezone =
+ findTimezoneByIdentifier(this.timezoneData, this.$input.val()) || defaultTimezone;
+
this.initDefaultTimezone();
this.initDropdown();
-
- this.onSelectTimezone = onSelectTimezone;
}
initDropdown() {
@@ -35,7 +46,7 @@ export default class TimezoneDropdown {
data: this.timezoneData,
filterable: true,
selectable: true,
- toggleLabel: item => item.name,
+ toggleLabel: this.displayFormat,
search: {
fields: ['name'],
},
@@ -43,20 +54,17 @@ export default class TimezoneDropdown {
text: item => formatTimezone(item),
});
- this.setDropdownToggle();
+ this.setDropdownToggle(this.displayFormat(this.initialTimezone));
}
initDefaultTimezone() {
- const initialValue = this.$input.val();
-
- if (!initialValue) {
- this.$input.val(defaultTimezone);
+ if (!this.$input.val()) {
+ this.$input.val(defaultTimezone.name);
}
}
- setDropdownToggle() {
- const initialValue = this.$input.val();
- this.$dropdownToggle.text(initialValue);
+ setDropdownToggle(dropdownText) {
+ this.$dropdownToggle.text(dropdownText);
}
updateInputValue({ selectedObj, e }) {
diff --git a/app/assets/javascripts/pages/projects/settings/operations/show/index.js b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
index 73c745179be..5270a7924ec 100644
--- a/app/assets/javascripts/pages/projects/settings/operations/show/index.js
+++ b/app/assets/javascripts/pages/projects/settings/operations/show/index.js
@@ -1,5 +1,7 @@
import mountErrorTrackingForm from '~/error_tracking_settings';
+import mountOperationSettings from '~/operation_settings';
document.addEventListener('DOMContentLoaded', () => {
mountErrorTrackingForm();
+ mountOperationSettings();
});
diff --git a/app/assets/javascripts/profile/profile.js b/app/assets/javascripts/profile/profile.js
index deacff5abe7..6e3800021b4 100644
--- a/app/assets/javascripts/profile/profile.js
+++ b/app/assets/javascripts/profile/profile.js
@@ -2,6 +2,9 @@ import $ from 'jquery';
import axios from '~/lib/utils/axios_utils';
import flash from '../flash';
import { parseBoolean } from '~/lib/utils/common_utils';
+import TimezoneDropdown, {
+ formatTimezone,
+} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
export default class Profile {
constructor({ form } = {}) {
@@ -10,6 +13,14 @@ export default class Profile {
this.setRepoRadio();
this.bindEvents();
this.initAvatarGlCrop();
+
+ this.$inputEl = $('#user_timezone');
+
+ this.timezoneDropdown = new TimezoneDropdown({
+ $inputEl: this.$inputEl,
+ $dropdownEl: $('.js-timezone-dropdown'),
+ displayFormat: selectedItem => formatTimezone(selectedItem),
+ });
}
initAvatarGlCrop() {
diff --git a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
index 40a873833e1..41e295387ae 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_access_dropdown.js
@@ -1,3 +1,5 @@
+import { __ } from '~/locale';
+
export default class ProtectedBranchAccessDropdown {
constructor(options) {
this.options = options;
@@ -15,7 +17,7 @@ export default class ProtectedBranchAccessDropdown {
if ($el.is('.is-active')) {
return item.text;
}
- return 'Select';
+ return __('Select');
},
clicked(options) {
options.e.preventDefault();
diff --git a/app/assets/javascripts/protected_branches/protected_branch_create.js b/app/assets/javascripts/protected_branches/protected_branch_create.js
index 48343c8ba0a..16ecd5523d6 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_create.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_create.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
import CreateItemDropdown from '../create_item_dropdown';
import AccessorUtilities from '../lib/utils/accessor';
+import { __ } from '~/locale';
export default class ProtectedBranchCreate {
constructor() {
@@ -35,7 +36,7 @@ export default class ProtectedBranchCreate {
this.createItemDropdown = new CreateItemDropdown({
$dropdown: $protectedBranchDropdown,
- defaultToggleLabel: 'Protected Branch',
+ defaultToggleLabel: __('Protected Branch'),
fieldName: 'protected_branch[name]',
onSelect: this.onSelectCallback,
getData: ProtectedBranchCreate.getProtectedBranches,
diff --git a/app/assets/javascripts/protected_branches/protected_branch_edit.js b/app/assets/javascripts/protected_branches/protected_branch_edit.js
index 5bc08f60d16..08d8c9919dd 100644
--- a/app/assets/javascripts/protected_branches/protected_branch_edit.js
+++ b/app/assets/javascripts/protected_branches/protected_branch_edit.js
@@ -1,6 +1,7 @@
import flash from '../flash';
import axios from '../lib/utils/axios_utils';
import ProtectedBranchAccessDropdown from './protected_branch_access_dropdown';
+import { __ } from '~/locale';
export default class ProtectedBranchEdit {
constructor(options) {
@@ -68,7 +69,7 @@ export default class ProtectedBranchEdit {
this.$allowedToPushDropdown.enable();
flash(
- 'Failed to update branch!',
+ __('Failed to update branch!'),
'alert',
document.querySelector('.js-protected-branches-list'),
);
diff --git a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
index c0659a0173a..10e2c8453e2 100644
--- a/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
+++ b/app/assets/javascripts/set_status_modal/set_status_modal_wrapper.vue
@@ -178,7 +178,7 @@ export default {
/>
<div ref="userStatusForm" class="form-group position-relative m-0">
<div class="input-group">
- <span class="input-group-btn">
+ <span class="input-group-prepend">
<button
ref="toggleEmojiMenuButton"
v-gl-tooltip.bottom
@@ -211,7 +211,7 @@ export default {
@keyup.enter.prevent
@click="hideEmojiMenu"
/>
- <span v-show="isDirty" class="input-group-btn">
+ <span v-show="isDirty" class="input-group-append">
<button
v-gl-tooltip.bottom
:title="s__('SetStatusModal|Clear status')"
diff --git a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
index f5a1ff2f6fd..f5fa68308bc 100644
--- a/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
+++ b/app/assets/javascripts/vue_merge_request_widget/components/mr_widget_pipeline.vue
@@ -94,8 +94,8 @@ export default {
</script>
<template>
- <div v-if="hasPipeline || hasCIError" class="ci-widget media js-ci-widget">
- <template v-if="hasCIError">
+ <div class="ci-widget media js-ci-widget">
+ <template v-if="!hasPipeline || hasCIError">
<div
class="add-border ci-status-icon ci-status-icon-failed ci-error js-ci-error append-right-default"
>
diff --git a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
index c5a2aa1f2af..32783b85df4 100644
--- a/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
+++ b/app/assets/javascripts/vue_shared/components/markdown/suggestion_diff_header.vue
@@ -1,8 +1,10 @@
<script>
import Icon from '~/vue_shared/components/icon.vue';
+import { GlButton, GlLoadingIcon, GlTooltipDirective } from '@gitlab/ui';
export default {
- components: { Icon },
+ components: { Icon, GlButton, GlLoadingIcon },
+ directives: { 'gl-tooltip': GlTooltipDirective },
props: {
canApply: {
type: Boolean,
@@ -21,7 +23,6 @@ export default {
},
data() {
return {
- isAppliedSuccessfully: false,
isApplying: false,
};
},
@@ -47,14 +48,19 @@ export default {
</a>
</div>
<span v-if="isApplied" class="badge badge-success">{{ __('Applied') }}</span>
- <button
- v-if="canApply"
- type="button"
- class="btn qa-apply-btn"
+ <div v-if="isApplying" class="d-flex align-items-center text-secondary">
+ <gl-loading-icon class="d-flex-center mr-2" />
+ <span>{{ __('Applying suggestion') }}</span>
+ </div>
+ <gl-button
+ v-else-if="canApply"
+ v-gl-tooltip.viewport="__('This also resolves the discussion')"
+ class="btn-inverted qa-apply-btn"
:disabled="isApplying"
+ variant="success"
@click="applySuggestion"
>
{{ __('Apply suggestion') }}
- </button>
+ </gl-button>
</div>
</template>
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index dffd5e70edb..2b499e50ea3 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -48,6 +48,10 @@
color: $brand-info;
}
+.bg-gray-light {
+ background-color: $gray-light;
+}
+
.text-break-word {
word-break: break-all;
}
@@ -446,19 +450,13 @@ img.emoji {
}
/** COMMON SPACING CLASSES **/
-.gl-pl-0 { padding-left: 0; }
-.gl-pl-1 { padding-left: #{0.5 * $grid-size}; }
-.gl-pl-2 { padding-left: $grid-size; }
-.gl-pl-3 { padding-left: #{2 * $grid-size}; }
-.gl-pl-4 { padding-left: #{3 * $grid-size}; }
-.gl-pl-5 { padding-left: #{4 * $grid-size}; }
-
-.gl-pr-0 { padding-right: 0; }
-.gl-pr-1 { padding-right: #{0.5 * $grid-size}; }
-.gl-pr-2 { padding-right: $grid-size; }
-.gl-pr-3 { padding-right: #{2 * $grid-size}; }
-.gl-pr-4 { padding-right: #{3 * $grid-size}; }
-.gl-pr-5 { padding-right: #{4 * $grid-size}; }
+@each $index, $padding in $spacing-scale {
+ #{'.gl-p-#{$index}'} { padding: $padding; }
+ #{'.gl-pl-#{$index}'} { padding-left: $padding; }
+ #{'.gl-pr-#{$index}'} { padding-right: $padding; }
+ #{'.gl-pt-#{$index}'} { padding-top: $padding; }
+ #{'.gl-pb-#{$index}'} { padding-bottom: $padding; }
+}
/**
* Removes browser specific clear icon from input fields in
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a6179e2a96e..a57cd6737f8 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -581,10 +581,15 @@
.emoji-menu-toggle-button {
@include emoji-menu-toggle-button;
+ padding: $gl-vert-padding $gl-btn-padding;
}
.input-group {
- height: 34px;
+ &,
+ .input-group-prepend,
+ .input-group-append {
+ height: $input-height;
+ }
}
}
diff --git a/app/assets/stylesheets/framework/modal.scss b/app/assets/stylesheets/framework/modal.scss
index 323a3dbecd5..e2bb1eb67c0 100644
--- a/app/assets/stylesheets/framework/modal.scss
+++ b/app/assets/stylesheets/framework/modal.scss
@@ -66,12 +66,6 @@
margin-top: $grid-size;
}
}
-
- @include media-breakpoint-up(sm) {
- .btn:nth-child(1) {
- margin-left: auto;
- }
- }
}
body.modal-open {
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 244b414d334..7c152efd9c7 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -473,3 +473,7 @@ textarea {
/* stylelint-enable */
.lh-100 { line-height: 1; }
+
+wbr {
+ display: inline-block;
+}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index da1f196afdb..4a034e1e547 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -11,6 +11,14 @@ $default-transition-duration: 0.15s;
$contextual-sidebar-width: 220px;
$contextual-sidebar-collapsed-width: 50px;
$toggle-sidebar-height: 48px;
+$spacing-scale: (
+ 0: 0,
+ 1: #{0.5 * $grid-size},
+ 2: $grid-size,
+ 3: #{2 * $grid-size},
+ 4: #{3 * $grid-size},
+ 5: #{4 * $grid-size}
+);
/*
* Color schema
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 09f75cd827f..f2b67a693c3 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -471,6 +471,11 @@ $note-form-margin-left: 72px;
vertical-align: top;
white-space: normal;
+ // Fixes subpixel rounding issue https://gitlab.com/gitlab-org/gitlab-ce/issues/53973
+ // background-color is needed for dark code preference
+ padding-bottom: 1px;
+ background-color: $white-light;
+
&.parallel {
border-width: 1px;
diff --git a/app/controllers/omniauth_callbacks_controller.rb b/app/controllers/omniauth_callbacks_controller.rb
index d9b3b4bbbd9..2a8dd997d04 100644
--- a/app/controllers/omniauth_callbacks_controller.rb
+++ b/app/controllers/omniauth_callbacks_controller.rb
@@ -86,7 +86,8 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
log_audit_event(current_user, with: oauth['provider'])
identity_linker ||= auth_module::IdentityLinker.new(current_user, oauth)
- identity_linker.link
+
+ link_identity(identity_linker)
if identity_linker.changed?
redirect_identity_linked
@@ -100,6 +101,10 @@ class OmniauthCallbacksController < Devise::OmniauthCallbacksController
end
end
+ def link_identity(identity_linker)
+ identity_linker.link
+ end
+
def redirect_identity_exists
redirect_to after_sign_in_path_for(current_user)
end
diff --git a/app/controllers/profiles/preferences_controller.rb b/app/controllers/profiles/preferences_controller.rb
index 0e30df1b15b..62f98d9e549 100644
--- a/app/controllers/profiles/preferences_controller.rb
+++ b/app/controllers/profiles/preferences_controller.rb
@@ -44,7 +44,9 @@ class Profiles::PreferencesController < Profiles::ApplicationController
:project_view,
:theme_id,
:first_day_of_week,
- :preferred_language
+ :preferred_language,
+ :time_display_relative,
+ :time_format_in_24h
]
end
end
diff --git a/app/controllers/profiles_controller.rb b/app/controllers/profiles_controller.rb
index b9c52618d4b..d3746248bd3 100644
--- a/app/controllers/profiles_controller.rb
+++ b/app/controllers/profiles_controller.rb
@@ -106,6 +106,7 @@ class ProfilesController < Profiles::ApplicationController
:organization,
:private_profile,
:include_private_contributions,
+ :timezone,
status: [:emoji, :message]
)
end
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index d8812c023ca..5a4adea497b 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -14,6 +14,7 @@ class Projects::EnvironmentsController < Projects::ApplicationController
push_frontend_feature_flag(:metrics_time_window)
push_frontend_feature_flag(:environment_metrics_use_prometheus_endpoint)
push_frontend_feature_flag(:environment_metrics_show_multiple_dashboards)
+ push_frontend_feature_flag(:grafana_dashboard_link)
end
def index
diff --git a/app/controllers/projects/settings/operations_controller.rb b/app/controllers/projects/settings/operations_controller.rb
index 5cfb0ac307d..b5c77e5bbf4 100644
--- a/app/controllers/projects/settings/operations_controller.rb
+++ b/app/controllers/projects/settings/operations_controller.rb
@@ -5,6 +5,10 @@ module Projects
class OperationsController < Projects::ApplicationController
before_action :authorize_update_environment!
+ before_action do
+ push_frontend_feature_flag(:grafana_dashboard_link)
+ end
+
helper_method :error_tracking_setting
def show
diff --git a/app/finders/issuable_finder.rb b/app/finders/issuable_finder.rb
index f1dd040515f..52b6e828cfa 100644
--- a/app/finders/issuable_finder.rb
+++ b/app/finders/issuable_finder.rb
@@ -29,6 +29,7 @@
# updated_after: datetime
# updated_before: datetime
# attempt_group_search_optimizations: boolean
+# attempt_project_search_optimizations: boolean
#
class IssuableFinder
prepend FinderWithCrossProjectAccess
@@ -184,7 +185,6 @@ class IssuableFinder
@project = project
end
- # rubocop: disable CodeReuse/ActiveRecord
def projects
return @projects if defined?(@projects)
@@ -192,17 +192,25 @@ class IssuableFinder
projects =
if current_user && params[:authorized_only].presence && !current_user_related?
- current_user.authorized_projects
+ current_user.authorized_projects(min_access_level)
elsif group
- finder_options = { include_subgroups: params[:include_subgroups], only_owned: true }
- GroupProjectsFinder.new(group: group, current_user: current_user, options: finder_options).execute # rubocop: disable CodeReuse/Finder
+ find_group_projects
else
- ProjectsFinder.new(current_user: current_user).execute # rubocop: disable CodeReuse/Finder
+ Project.public_or_visible_to_user(current_user, min_access_level)
end
- @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil)
+ @projects = projects.with_feature_available_for_user(klass, current_user).reorder(nil) # rubocop: disable CodeReuse/ActiveRecord
+ end
+
+ def find_group_projects
+ return Project.none unless group
+
+ if params[:include_subgroups]
+ Project.where(namespace_id: group.self_and_descendants) # rubocop: disable CodeReuse/ActiveRecord
+ else
+ group.projects
+ end.public_or_visible_to_user(current_user, min_access_level)
end
- # rubocop: enable CodeReuse/ActiveRecord
def search
params[:search].presence
@@ -570,4 +578,8 @@ class IssuableFinder
scope = params[:scope]
scope == 'created_by_me' || scope == 'authored' || scope == 'assigned_to_me'
end
+
+ def min_access_level
+ ProjectFeature.required_minimum_access_level(klass)
+ end
end
diff --git a/app/finders/issues_finder.rb b/app/finders/issues_finder.rb
index e6a82f55856..58a01d598ba 100644
--- a/app/finders/issues_finder.rb
+++ b/app/finders/issues_finder.rb
@@ -48,9 +48,9 @@ class IssuesFinder < IssuableFinder
OR (issues.confidential = TRUE
AND (issues.author_id = :user_id
OR EXISTS (SELECT TRUE FROM issue_assignees WHERE user_id = :user_id AND issue_id = issues.id)
- OR issues.project_id IN(:project_ids)))',
+ OR EXISTS (:authorizations)))',
user_id: current_user.id,
- project_ids: current_user.authorized_projects(CONFIDENTIAL_ACCESS_LEVEL).select(:id))
+ authorizations: current_user.authorizations_for_projects(min_access_level: CONFIDENTIAL_ACCESS_LEVEL, related_project_column: "issues.project_id"))
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/finders/projects_finder.rb b/app/finders/projects_finder.rb
index 93d3c991846..23b731b1aed 100644
--- a/app/finders/projects_finder.rb
+++ b/app/finders/projects_finder.rb
@@ -62,7 +62,7 @@ class ProjectsFinder < UnionFinder
collection = by_personal(collection)
collection = by_starred(collection)
collection = by_trending(collection)
- collection = by_visibilty_level(collection)
+ collection = by_visibility_level(collection)
collection = by_tags(collection)
collection = by_search(collection)
collection = by_archived(collection)
@@ -71,12 +71,11 @@ class ProjectsFinder < UnionFinder
collection
end
- # rubocop: disable CodeReuse/ActiveRecord
def collection_with_user
if owned_projects?
current_user.owned_projects
elsif min_access_level?
- current_user.authorized_projects.where('project_authorizations.access_level >= ?', params[:min_access_level])
+ current_user.authorized_projects(params[:min_access_level])
else
if private_only?
current_user.authorized_projects
@@ -85,7 +84,6 @@ class ProjectsFinder < UnionFinder
end
end
end
- # rubocop: enable CodeReuse/ActiveRecord
# Builds a collection for an anonymous user.
def collection_without_user
@@ -131,7 +129,7 @@ class ProjectsFinder < UnionFinder
end
# rubocop: disable CodeReuse/ActiveRecord
- def by_visibilty_level(items)
+ def by_visibility_level(items)
params[:visibility_level].present? ? items.where(visibility_level: params[:visibility_level]) : items
end
# rubocop: enable CodeReuse/ActiveRecord
diff --git a/app/models/concerns/has_status.rb b/app/models/concerns/has_status.rb
index 8882f48c281..78bcce2f592 100644
--- a/app/models/concerns/has_status.rb
+++ b/app/models/concerns/has_status.rb
@@ -66,6 +66,10 @@ module HasStatus
def all_state_names
state_machines.values.flat_map(&:states).flat_map { |s| s.map(&:name) }
end
+
+ def completed_statuses
+ COMPLETED_STATUSES.map(&:to_sym)
+ end
end
included do
diff --git a/app/models/project.rb b/app/models/project.rb
index da72186c8a0..61d245478ca 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -464,10 +464,12 @@ class Project < ApplicationRecord
# Returns a collection of projects that is either public or visible to the
# logged in user.
- def self.public_or_visible_to_user(user = nil)
+ def self.public_or_visible_to_user(user = nil, min_access_level = nil)
+ min_access_level = nil if user&.admin?
+
if user
where('EXISTS (?) OR projects.visibility_level IN (?)',
- user.authorizations_for_projects,
+ user.authorizations_for_projects(min_access_level: min_access_level),
Gitlab::VisibilityLevel.levels_for_user(user))
else
public_to_user
@@ -477,30 +479,32 @@ class Project < ApplicationRecord
# project features may be "disabled", "internal", "enabled" or "public". If "internal",
# they are only available to team members. This scope returns projects where
# the feature is either public, enabled, or internal with permission for the user.
+ # Note: this scope doesn't enforce that the user has access to the projects, it just checks
+ # that the user has access to the feature. It's important to use this scope with others
+ # that checks project authorizations first.
#
# This method uses an optimised version of `with_feature_access_level` for
# logged in users to more efficiently get private projects with the given
# feature.
def self.with_feature_available_for_user(feature, user)
visible = [ProjectFeature::ENABLED, ProjectFeature::PUBLIC]
- min_access_level = ProjectFeature.required_minimum_access_level(feature)
if user&.admin?
with_feature_enabled(feature)
elsif user
+ min_access_level = ProjectFeature.required_minimum_access_level(feature)
column = ProjectFeature.quoted_access_level_column(feature)
with_project_feature
- .where(
- "(projects.visibility_level > :private AND (#{column} IS NULL OR #{column} >= (:public_visible) OR (#{column} = :private_visible AND EXISTS(:authorizations))))"\
- " OR (projects.visibility_level = :private AND (#{column} IS NULL OR #{column} >= :private_visible) AND EXISTS(:authorizations))",
- {
- private: Gitlab::VisibilityLevel::PRIVATE,
- public_visible: ProjectFeature::ENABLED,
- private_visible: ProjectFeature::PRIVATE,
- authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
- })
+ .where("#{column} IS NULL OR #{column} IN (:public_visible) OR (#{column} = :private_visible AND EXISTS (:authorizations))",
+ {
+ public_visible: visible,
+ private_visible: ProjectFeature::PRIVATE,
+ authorizations: user.authorizations_for_projects(min_access_level: min_access_level)
+ })
else
+ # This has to be added to include features whose value is nil in the db
+ visible << nil
with_feature_access_level(feature, visible)
end
end
diff --git a/app/models/remote_mirror.rb b/app/models/remote_mirror.rb
index cbfc1a7c1b2..af705b29f7a 100644
--- a/app/models/remote_mirror.rb
+++ b/app/models/remote_mirror.rb
@@ -133,6 +133,10 @@ class RemoteMirror < ApplicationRecord
end
alias_method :enabled?, :enabled
+ def disabled?
+ !enabled?
+ end
+
def updated_since?(timestamp)
last_update_started_at && last_update_started_at > timestamp && !update_failed?
end
diff --git a/app/models/user.rb b/app/models/user.rb
index 43039f3760e..60f69659a6b 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -230,6 +230,9 @@ class User < ApplicationRecord
delegate :notes_filter_for, to: :user_preference
delegate :set_notes_filter, to: :user_preference
delegate :first_day_of_week, :first_day_of_week=, to: :user_preference
+ delegate :timezone, :timezone=, to: :user_preference
+ delegate :time_display_relative, :time_display_relative=, to: :user_preference
+ delegate :time_format_in_24h, :time_format_in_24h=, to: :user_preference
accepts_nested_attributes_for :user_preference, update_only: true
@@ -757,11 +760,15 @@ class User < ApplicationRecord
# Typically used in conjunction with projects table to get projects
# a user has been given access to.
+ # The param `related_project_column` is the column to compare to the
+ # project_authorizations. By default is projects.id
#
# Example use:
# `Project.where('EXISTS(?)', user.authorizations_for_projects)`
- def authorizations_for_projects(min_access_level: nil)
- authorizations = project_authorizations.select(1).where('project_authorizations.project_id = projects.id')
+ def authorizations_for_projects(min_access_level: nil, related_project_column: 'projects.id')
+ authorizations = project_authorizations
+ .select(1)
+ .where("project_authorizations.project_id = #{related_project_column}")
return authorizations unless min_access_level.present?
diff --git a/app/models/user_preference.rb b/app/models/user_preference.rb
index 282b192167f..f1326f4c8cb 100644
--- a/app/models/user_preference.rb
+++ b/app/models/user_preference.rb
@@ -10,6 +10,10 @@ class UserPreference < ApplicationRecord
validates :issue_notes_filter, :merge_request_notes_filter, inclusion: { in: NOTES_FILTERS.values }, presence: true
+ default_value_for :timezone, value: Time.zone.tzinfo.name, allows_nil: false
+ default_value_for :time_display_relative, value: true, allows_nil: false
+ default_value_for :time_format_in_24h, value: false, allows_nil: false
+
class << self
def notes_filters
{
diff --git a/app/policies/group_policy.rb b/app/policies/group_policy.rb
index eb2e536e8e9..ea86858181d 100644
--- a/app/policies/group_policy.rb
+++ b/app/policies/group_policy.rb
@@ -129,6 +129,10 @@ class GroupPolicy < BasePolicy
def access_level
return GroupMember::NO_ACCESS if @user.nil?
- @access_level ||= @subject.max_member_access_for_user(@user)
+ @access_level ||= lookup_access_level!
+ end
+
+ def lookup_access_level!
+ @subject.max_member_access_for_user(@user)
end
end
diff --git a/app/uploaders/import_export_uploader.rb b/app/uploaders/import_export_uploader.rb
index 716922bc017..104d5d3b3dd 100644
--- a/app/uploaders/import_export_uploader.rb
+++ b/app/uploaders/import_export_uploader.rb
@@ -7,10 +7,6 @@ class ImportExportUploader < AttachmentUploader
EXTENSION_WHITELIST
end
- def move_to_store
- true
- end
-
def move_to_cache
false
end
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index b950e53639a..c2116ec63dd 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -46,6 +46,7 @@
= _('Contribution Analytics')
= render_if_exists 'layouts/nav/group_insights_link'
+ = render_if_exists 'groups/sidebar/dependency_proxy' # EE-specific
= render_if_exists "layouts/nav/ee/epic_link", group: @group
diff --git a/app/views/profiles/preferences/show.html.haml b/app/views/profiles/preferences/show.html.haml
index bfe1c3ddf33..58f2eb229ba 100644
--- a/app/views/profiles/preferences/show.html.haml
+++ b/app/views/profiles/preferences/show.html.haml
@@ -82,5 +82,31 @@
= f.label :first_day_of_week, class: 'label-bold' do
= _('First day of the week')
= f.select :first_day_of_week, first_day_of_week_choices_with_default, {}, class: 'form-control'
+ - if Feature.enabled?(:user_time_settings)
+ .col-sm-12
+ %hr
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0= s_('Preferences|Time preferences')
+ %p= s_('Preferences|These settings will update how dates and times are displayed for you.')
+ .col-lg-8
+ .form-group
+ %h5= s_('Preferences|Time format')
+ .checkbox-icon-inline-wrapper.form-check
+ - time_format_label = capture do
+ = s_('Preferences|Display time in 24-hour format')
+ = f.check_box :time_format_in_24h, class: 'form-check-input'
+ = f.label :time_format_in_24h do
+ = time_format_label
+ %h5= s_('Preferences|Time display')
+ .checkbox-icon-inline-wrapper.form-check
+ - time_display_label = capture do
+ = s_('Preferences|Use relative times')
+ = f.check_box :time_display_relative, class: 'form-check-input'
+ = f.label :time_display_relative do
+ = time_display_label
+ .text-muted
+ = s_('Preferences|For example: 30 mins ago.')
+ .col-lg-4.profile-settings-sidebar
+ .col-lg-8
.form-group
= f.submit _('Save changes'), class: 'btn btn-success'
diff --git a/app/views/profiles/show.html.haml b/app/views/profiles/show.html.haml
index 02c750a92c3..45b6453b5ff 100644
--- a/app/views/profiles/show.html.haml
+++ b/app/views/profiles/show.html.haml
@@ -64,6 +64,18 @@
prepend: emoji_button,
append: reset_message_button,
placeholder: s_("Profiles|What's your status?")
+ - if Feature.enabled?(:user_time_settings)
+ %hr
+ .row.user-time-preferences
+ .col-lg-4.profile-settings-sidebar
+ %h4.prepend-top-0= s_("Profiles|Time settings")
+ %p= s_("Profiles|You can set your current timezone here")
+ .col-lg-8
+ -# TODO: might need an entry in user/profile.md to describe some of these settings
+ -# https://gitlab.com/gitlab-org/gitlab-ce/issues/60070
+ %h5= ("Time zone")
+ = dropdown_tag(_("Select a timezone"), options: { toggle_class: 'btn js-timezone-dropdown input-lg', title: _("Select a timezone"), filter: true, placeholder: s_("OfSearchInADropdown|Filter"), data: { data: timezone_data } } )
+ %input.hidden{ :type => 'hidden', :id => 'user_timezone', :name => 'user[timezone]', value: @user.timezone }
%hr
.row
diff --git a/app/views/projects/issues/show.html.haml b/app/views/projects/issues/show.html.haml
index 715c36fa9aa..d55afee4523 100644
--- a/app/views/projects/issues/show.html.haml
+++ b/app/views/projects/issues/show.html.haml
@@ -79,7 +79,7 @@
= render_if_exists 'projects/issues/related_issues'
- #js-related-merge-requests{ data: { endpoint: expose_url(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
+ #js-related-merge-requests{ data: { endpoint: expose_path(api_v4_projects_issues_related_merge_requests_path(id: @project.id, issue_iid: @issue.iid)), project_namespace: @project.namespace.path, project_path: @project.path } }
- if can?(current_user, :download_code, @project)
#related-branches{ data: { url: related_branches_project_issue_path(@project, @issue) } }
diff --git a/app/views/projects/mirrors/_disabled_mirror_badge.html.haml b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml
new file mode 100644
index 00000000000..356cb43f07f
--- /dev/null
+++ b/app/views/projects/mirrors/_disabled_mirror_badge.html.haml
@@ -0,0 +1 @@
+.badge.badge-warning.qa-disabled-mirror-badge{ data: { toggle: 'tooltip', html: 'true' }, title: _('Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them.') }= _('Disabled')
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 0cd00d3e708..73e2a4ffb8b 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -49,17 +49,19 @@
%tbody.js-mirrors-table-body
= render_if_exists 'projects/mirrors/table_pull_row'
- @project.remote_mirrors.each_with_index do |mirror, index|
- - if mirror.enabled
- %tr.qa-mirrored-repository-row
- %td.qa-mirror-repository-url= mirror.safe_url
- %td= _('Push')
- %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
- %td
- - if mirror.last_error.present?
- .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
- %td
- .btn-group.mirror-actions-group.pull-right{ role: 'group' }
- - if mirror.ssh_key_auth?
- = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'))
- = render 'shared/remote_mirror_update_button', remote_mirror: mirror
- %button.js-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
+ - next if mirror.new_record?
+ %tr.qa-mirrored-repository-row{ class: ('bg-secondary' if mirror.disabled?) }
+ %td.qa-mirror-repository-url= mirror.safe_url
+ %td= _('Push')
+ %td.qa-mirror-last-update-at= mirror.last_update_at.present? ? time_ago_with_tooltip(mirror.last_update_at) : _('Never')
+ %td
+ - if mirror.disabled?
+ = render 'projects/mirrors/disabled_mirror_badge'
+ - if mirror.last_error.present?
+ .badge.mirror-error-badge{ data: { toggle: 'tooltip', html: 'true' }, title: html_escape(mirror.last_error.try(:strip)) }= _('Error')
+ %td
+ .btn-group.mirror-actions-group.pull-right{ role: 'group' }
+ - if mirror.ssh_key_auth?
+ = clipboard_button(text: mirror.ssh_public_key, class: 'btn btn-default', title: _('Copy SSH public key'))
+ = render 'shared/remote_mirror_update_button', remote_mirror: mirror
+ %button.js-delete-mirror.qa-delete-mirror.btn.btn-danger{ type: 'button', data: { mirror_id: mirror.id, toggle: 'tooltip', container: 'body' }, title: _('Remove') }= icon('trash-o')
diff --git a/app/views/projects/settings/operations/_external_dashboard.html.haml b/app/views/projects/settings/operations/_external_dashboard.html.haml
new file mode 100644
index 00000000000..2fbb9195a04
--- /dev/null
+++ b/app/views/projects/settings/operations/_external_dashboard.html.haml
@@ -0,0 +1,2 @@
+.js-operation-settings{ data: { external_dashboard: { path: '',
+ help_page_path: help_page_path('user/project/operations/link_to_external_dashboard') } } }
diff --git a/app/views/projects/settings/operations/show.html.haml b/app/views/projects/settings/operations/show.html.haml
index 6f777305a54..edc2c58a8ed 100644
--- a/app/views/projects/settings/operations/show.html.haml
+++ b/app/views/projects/settings/operations/show.html.haml
@@ -4,4 +4,5 @@
= render_if_exists 'projects/settings/operations/incidents'
= render 'projects/settings/operations/error_tracking', expanded: true
+= render 'projects/settings/operations/external_dashboard'
= render_if_exists 'projects/settings/operations/tracing'
diff --git a/app/views/shared/_remote_mirror_update_button.html.haml b/app/views/shared/_remote_mirror_update_button.html.haml
index 721a2af8069..8da2ae5111a 100644
--- a/app/views/shared/_remote_mirror_update_button.html.haml
+++ b/app/views/shared/_remote_mirror_update_button.html.haml
@@ -1,6 +1,6 @@
- if remote_mirror.update_in_progress?
%button.btn.disabled{ type: 'button', data: { toggle: 'tooltip', container: 'body' }, title: _('Updating') }
= icon("refresh spin")
-- else
+- elsif remote_mirror.enabled?
= link_to update_now_project_mirror_path(@project, sync_remote: true), method: :post, class: "btn qa-update-now-button", data: { toggle: 'tooltip', container: 'body' }, title: _('Update now') do
= icon("refresh")
diff --git a/changelogs/unreleased/10808-allow-license-import-during-install.yml b/changelogs/unreleased/10808-allow-license-import-during-install.yml
new file mode 100644
index 00000000000..f93edf03d51
--- /dev/null
+++ b/changelogs/unreleased/10808-allow-license-import-during-install.yml
@@ -0,0 +1,5 @@
+---
+title: Document EE License Auto Import During Install
+merge_request: 28106
+author:
+type: other
diff --git a/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml
new file mode 100644
index 00000000000..2fbacbcb011
--- /dev/null
+++ b/changelogs/unreleased/28119-remove-note-multi-line-suggestions.yml
@@ -0,0 +1,5 @@
+---
+title: Remove the note in the docs that multi-line suggestions are not yet available
+merge_request: 28119
+author: hardysim
+type: other
diff --git a/changelogs/unreleased/53973-fix-subpixel-border-issue.yml b/changelogs/unreleased/53973-fix-subpixel-border-issue.yml
new file mode 100644
index 00000000000..0dae7047236
--- /dev/null
+++ b/changelogs/unreleased/53973-fix-subpixel-border-issue.yml
@@ -0,0 +1,5 @@
+---
+title: Fix MR discussion border missing in chrome sometimes
+merge_request: 28185
+author:
+type: fixed
diff --git a/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml
new file mode 100644
index 00000000000..862ce623d8c
--- /dev/null
+++ b/changelogs/unreleased/54405-resolve-discussion-when-applying-a-suggested-change.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve discussion when apply suggestion
+merge_request: 28160
+author:
+type: changed
diff --git a/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml
new file mode 100644
index 00000000000..ac3bb596842
--- /dev/null
+++ b/changelogs/unreleased/55127-add-delay-after-mr-creation-for-async-tasks-to-complete.yml
@@ -0,0 +1,5 @@
+---
+title: Wait for pipeline creation to complete before accepting a MR via API
+merge_request: 27978
+author: kerrizor
+type: fixed
diff --git a/changelogs/unreleased/57077-add-salesforce-omniauth.yml b/changelogs/unreleased/57077-add-salesforce-omniauth.yml
new file mode 100644
index 00000000000..ebd0637ddac
--- /dev/null
+++ b/changelogs/unreleased/57077-add-salesforce-omniauth.yml
@@ -0,0 +1,5 @@
+---
+title: Resolve Salesforce.com omniauth support
+merge_request: 27834
+author:
+type: added
diff --git a/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml
new file mode 100644
index 00000000000..f4ce3a51724
--- /dev/null
+++ b/changelogs/unreleased/57654-add-time-preferences-for-user-fe.yml
@@ -0,0 +1,5 @@
+---
+title: Add time preferences for user
+merge_request: 25381
+author:
+type: added
diff --git a/changelogs/unreleased/60462-empty-pipeline-section.yml b/changelogs/unreleased/60462-empty-pipeline-section.yml
new file mode 100644
index 00000000000..7d90215e20c
--- /dev/null
+++ b/changelogs/unreleased/60462-empty-pipeline-section.yml
@@ -0,0 +1,5 @@
+---
+title: Fix empty block in MR widget when user doesn't have permission
+merge_request: 27462
+author:
+type: fixed
diff --git a/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml b/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml
new file mode 100644
index 00000000000..4126b8f93c1
--- /dev/null
+++ b/changelogs/unreleased/61494-set-status-modal-visual-bugs.yml
@@ -0,0 +1,5 @@
+---
+title: Fix visual issues in set status modal
+merge_request: 28147
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-11430-update_clair_local_scan.yml b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml
new file mode 100644
index 00000000000..04bb04c3919
--- /dev/null
+++ b/changelogs/unreleased/ce-11430-update_clair_local_scan.yml
@@ -0,0 +1,5 @@
+---
+title: Update clair-local-scan to v2.0.8 for container scanning
+merge_request: 27977
+author:
+type: other
diff --git a/changelogs/unreleased/fix-js-error-ssh-key-view.yml b/changelogs/unreleased/fix-js-error-ssh-key-view.yml
new file mode 100644
index 00000000000..0615f2ee217
--- /dev/null
+++ b/changelogs/unreleased/fix-js-error-ssh-key-view.yml
@@ -0,0 +1,5 @@
+---
+title: disable SSH key validation in key details view
+merge_request: 28180
+author: Roger Meier
+type: fixed
diff --git a/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml
new file mode 100644
index 00000000000..c513f3c3aeb
--- /dev/null
+++ b/changelogs/unreleased/fj-59522-improve-search-controller-performance.yml
@@ -0,0 +1,5 @@
+---
+title: Add improvements to global search of issues and merge requests
+merge_request: 27817
+author:
+type: performance
diff --git a/changelogs/unreleased/friendly-wrap-component.yml b/changelogs/unreleased/friendly-wrap-component.yml
new file mode 100644
index 00000000000..c16ca0af287
--- /dev/null
+++ b/changelogs/unreleased/friendly-wrap-component.yml
@@ -0,0 +1,5 @@
+---
+title: Add CSS fix for <wbr> elements on IE11
+merge_request: 27846
+author:
+type: other
diff --git a/changelogs/unreleased/gitaly-version-v1.42.0.yml b/changelogs/unreleased/gitaly-version-v1.42.0.yml
new file mode 100644
index 00000000000..38621fa071e
--- /dev/null
+++ b/changelogs/unreleased/gitaly-version-v1.42.0.yml
@@ -0,0 +1,5 @@
+---
+title: Upgrade to Gitaly v1.42.0
+merge_request: 28135
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-cleanup-import-export.yml b/changelogs/unreleased/sh-cleanup-import-export.yml
new file mode 100644
index 00000000000..3d5d6f3c907
--- /dev/null
+++ b/changelogs/unreleased/sh-cleanup-import-export.yml
@@ -0,0 +1,5 @@
+---
+title: Clean up CarrierWave's import/export files
+merge_request: 27487
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-fix-related-merge-requests-path.yml b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml
new file mode 100644
index 00000000000..4b4108feda4
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-related-merge-requests-path.yml
@@ -0,0 +1,5 @@
+---
+title: Use a path for the related merge requests endpoint
+merge_request: 28171
+author:
+type: fixed
diff --git a/changelogs/unreleased/shell-9-1-0.yml b/changelogs/unreleased/shell-9-1-0.yml
new file mode 100644
index 00000000000..d5a01ee57ee
--- /dev/null
+++ b/changelogs/unreleased/shell-9-1-0.yml
@@ -0,0 +1,5 @@
+---
+title: Update gitlab-shell to v9.1.0
+merge_request: 28184
+author:
+type: other
diff --git a/changelogs/unreleased/show-disabled-mirrors.yml b/changelogs/unreleased/show-disabled-mirrors.yml
new file mode 100644
index 00000000000..a401606b331
--- /dev/null
+++ b/changelogs/unreleased/show-disabled-mirrors.yml
@@ -0,0 +1,5 @@
+---
+title: Show disabled project repo mirrors in settings
+merge_request: 27326
+author:
+type: other
diff --git a/changelogs/unreleased/winh-notes-error-handling.yml b/changelogs/unreleased/winh-notes-error-handling.yml
new file mode 100644
index 00000000000..6f23dd459d4
--- /dev/null
+++ b/changelogs/unreleased/winh-notes-error-handling.yml
@@ -0,0 +1,5 @@
+---
+title: Handle errors in successful notes reply
+merge_request: 28082
+author:
+type: fixed
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 2f822805b25..bff809b7661 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -940,6 +940,10 @@ test:
app_id: 'YOUR_CLIENT_ID',
app_secret: 'YOUR_CLIENT_SECRET',
args: { scope: 'aq:name email~rs address aq:push' } }
+ - { name: 'salesforce',
+ app_id: 'YOUR_CLIENT_ID',
+ app_secret: 'YOUR_CLIENT_SECRET'
+ }
ldap:
enabled: false
servers:
diff --git a/db/migrate/20190325105715_add_fields_to_user_preferences.rb b/db/migrate/20190325105715_add_fields_to_user_preferences.rb
new file mode 100644
index 00000000000..9ea3b4f9cd8
--- /dev/null
+++ b/db/migrate/20190325105715_add_fields_to_user_preferences.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddFieldsToUserPreferences < ActiveRecord::Migration[5.0]
+ include Gitlab::Database::MigrationHelpers
+
+ disable_ddl_transaction!
+
+ DOWNTIME = false
+
+ def up
+ add_column(:user_preferences, :timezone, :string)
+ add_column(:user_preferences, :time_display_relative, :boolean)
+ add_column(:user_preferences, :time_format_in_24h, :boolean)
+ end
+
+ def down
+ remove_column(:user_preferences, :timezone)
+ remove_column(:user_preferences, :time_display_relative)
+ remove_column(:user_preferences, :time_format_in_24h)
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index deaf406fe3d..1be3afd5282 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -2246,6 +2246,9 @@ ActiveRecord::Schema.define(version: 20190506135400) do
t.integer "first_day_of_week"
t.string "issues_sort"
t.string "merge_requests_sort"
+ t.string "timezone"
+ t.boolean "time_display_relative"
+ t.boolean "time_format_in_24h"
t.index ["user_id"], name: "index_user_preferences_on_user_id", unique: true, using: :btree
end
diff --git a/doc/administration/auth/ldap.md b/doc/administration/auth/ldap.md
index 423a79c56d8..54279897e04 100644
--- a/doc/administration/auth/ldap.md
+++ b/doc/administration/auth/ldap.md
@@ -30,9 +30,9 @@ the LDAP server.
### User deletion
-If a user is deleted from the LDAP server, they will be blocked in GitLab, as
+If a user is deleted from the LDAP server, they will be blocked in GitLab as
well. Users will be immediately blocked from logging in. However, there is an
-LDAP check cache time (sync time) of one hour (see note). This means users that
+LDAP check cache time of one hour (see note) which means users that
are already logged in or are using Git over SSH will still be able to access
GitLab for up to one hour. Manually block the user in the GitLab Admin area to
immediately block all access.
diff --git a/doc/administration/high_availability/README.md b/doc/administration/high_availability/README.md
index de61c01991b..24db1c28778 100644
--- a/doc/administration/high_availability/README.md
+++ b/doc/administration/high_availability/README.md
@@ -145,14 +145,38 @@ environments this is a good architecture to consider if you foresee or do have
contention due to certain workloads.
- 3 PostgreSQL nodes
+- 1 PgBouncer node
- 2 Redis nodes
- 3 Consul/Sentinel nodes
- 2 or more Sidekiq nodes
-- 2 or more Web nodes (Unicorn, Workhorse, PGBouncer)
+- 2 or more GitLab application nodes (Unicorn, Workhorse)
- 1 or more NFS/Gitaly servers
+- 1 Monitoring node (Prometheus, Grafana)
![Hybrid architecture diagram](https://docs.gitlab.com/ee/administration/img/high_availability/hybrid.png)
+#### Reference Architecture
+
+- **Status:** Work-in-progress
+- **Supported Users (approximate):** 10,000
+- **Related Issues:** [gitlab-com/support/support-team-meta#1513](https://gitlab.com/gitlab-com/support/support-team-meta/issues/1513),
+ [gitlab-org/quality/team-tasks#110](https://gitlab.com/gitlab-org/quality/team-tasks/issues/110)
+
+The Support and Quality teams are in the process of building and performance testing
+an environment that will support about 10,000 users. The specifications below
+are a work-in-progress representation of the work so far. Quality will be
+certifying this environment in FY20-Q2. The specifications may be adjusted
+prior to certification based on performance testing.
+
+- 3 PostgreSQL - 4 CPU, 8GB RAM
+- 1 PgBouncer - 2 CPU, 4GB RAM
+- 2 Redis - 2 CPU, 8GB RAM
+- 3 Consul/Sentinel - 2 CPU, 2GB RAM
+- 4 Sidekiq - 4 CPU, 8GB RAM
+- 5 GitLab application nodes - 20 CPU, 64GB RAM
+- 1 Gitaly - 20 CPU, 64GB RAM
+- 1 Monitoring node - 4 CPU, 8GB RAM
+
### Fully Distributed
This architecture scales to hundreds of thousands of users and projects and is
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 02e88dbd2a6..797a7242bd0 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -40,6 +40,7 @@ Learn how to install, configure, update, and maintain your GitLab instance.
- [Geo](https://docs.gitlab.com/ee/administration/geo/replication/index.html): Replicate your GitLab instance to other geographic locations as a read-only fully operational version. **[PREMIUM ONLY]**
- [Disaster Recovery](https://docs.gitlab.com/ee/administration/geo/disaster_recovery/index.html): Quickly fail-over to a different site with minimal effort in a disaster situation. **[PREMIUM ONLY]**
- [Pivotal Tile](https://docs.gitlab.com/ee/install/pivotal/index.html): Deploy GitLab as a pre-configured appliance using Ops Manager (BOSH) for Pivotal Cloud Foundry. **[PREMIUM ONLY]**
+- [Add License](../user/admin_area/license.md): Upload a license at install time to unlock features that are in paid tiers of GitLab. **[STARTER ONLY]**
### Configuring GitLab
diff --git a/doc/administration/monitoring/prometheus/gitlab_metrics.md b/doc/administration/monitoring/prometheus/gitlab_metrics.md
index c243dd9edbb..48bd709a2b7 100644
--- a/doc/administration/monitoring/prometheus/gitlab_metrics.md
+++ b/doc/administration/monitoring/prometheus/gitlab_metrics.md
@@ -43,10 +43,11 @@ The following metrics are available:
| redis_ping_latency_seconds | Gauge | 9.4 | Round trip time of the redis ping |
| user_session_logins_total | Counter | 9.4 | Counter of how many users have logged in |
| upload_file_does_not_exist | Counter | 10.7 in EE, 11.5 in CE | Number of times an upload record could not find its file |
-| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
-| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
-| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
-| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| failed_login_captcha_total | Gauge | 11.0 | Counter of failed CAPTCHA attempts during login |
+| successful_login_captcha_total | Gauge | 11.0 | Counter of successful CAPTCHA attempts during login |
+| unicorn_active_connections | Gauge | 11.0 | The number of active Unicorn connections (workers) |
+| unicorn_queued_connections | Gauge | 11.0 | The number of queued Unicorn connections |
+| unicorn_workers | Gauge | 11.11 | The number of Unicorn workers |
## Sidekiq Metrics available for Geo **[PREMIUM]**
@@ -100,6 +101,10 @@ Some basic Ruby runtime metrics are available:
| ruby_file_descriptors | Gauge | 11.1 | File descriptors per process |
| ruby_memory_bytes | Gauge | 11.1 | Memory usage by process |
| ruby_sampler_duration_seconds_total | Counter | 11.1 | Time spent collecting stats |
+| ruby_process_cpu_seconds_total | Gauge | 11.11 | Total amount of CPU time per process |
+| ruby_process_max_fds | Gauge | 11.11 | Maximum number of open file descriptors per process |
+| ruby_process_resident_memory_bytes | Gauge | 11.11 | Memory usage by process, measured in bytes |
+| ruby_process_start_time_seconds | Gauge | 11.11 | The elapsed time between system boot and the process started, measured in seconds |
[GC.stat]: https://ruby-doc.org/core-2.3.0/GC.html#method-c-stat
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 99e4c64ff86..dca2d953286 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -2078,8 +2078,8 @@ of the `group/my-project`:
```yaml
include:
- - local: : /templates/docker-build.yml
- - local: : /templates/docker-testing.yml
+ - local: /templates/docker-build.yml
+ - local: /templates/docker-testing.yml
```
Our `/templates/docker-build.yml` present in `group/my-project` adds a `docker-build` job:
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 115c8cfb9ff..8d1879e2944 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -210,12 +210,156 @@ To serve repositories over SSH there's an add-on application called gitlab-shell
### Components
-<img src="https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/pub?w=987&amp;h=797">
+```mermaid
+graph TB
+
+ HTTP[HTTP/HTTPS] -- TCP 80, 443 --> NGINX(NGINX)
+ SSH --TCP 22 --> GitLabShell(GitLab Shell)
+ SMTP(SMTP Gateway)
+ Geo(GitLab Geo Node) -- TCP 22, 80, 443 --> NGINX
+
+ subgraph GitLab
+ GitLabShell --TCP 8080 -->Unicorn["Unicorn (GitLab Rails)"]
+ GitLabShell --> Gitaly
+ GitLabShell --> Redis
+ Unicorn --> PgBouncer(PgBouncer)
+ Unicorn --> Redis
+ Unicorn --> Gitaly
+ Redis --> Sidekiq
+ Sidekiq("Sidekiq (GitLab Rails, ES Indexer)") --> PgBouncer
+ GitLabWorkhorse(GitLab Workhorse) --> Unicorn
+ GitLabWorkhorse --> Redis
+ GitLabWorkhorse --> Gitaly
+ Gitaly --> Redis
+ NGINX --> GitLabWorkhorse
+ NGINX -- TCP 8090 --> GitLabPages(GitLab Pages)
+ NGINX --> Grafana(Grafana)
+ Grafana -- TCP 9090 --> Prometheus(Prometheus)
+ Prometheus -- TCP 80, 443 --> Unicorn
+ RedisExporter(Redis Exporter) --> Redis
+ Prometheus -- TCP 9121 --> RedisExporter
+ PostgreSQLExporter(PostgreSQL Exporter) --> PostgreSQL
+ PgBouncerExporter(PgBouncer Exporter) --> PgBouncer
+ Prometheus -- TCP 9187 --> PostgreSQLExporter
+ Prometheus -- TCP 9100 --> NodeExporter(Node Exporter)
+ Prometheus -- TCP 9168 --> GitLabMonitor(GitLab Monitor)
+ Prometheus -- TCP 9127 --> PgBouncerExporter
+ GitLabMonitor --> PostgreSQL
+ GitLabMonitor --> GitLabShell
+ GitLabMonitor --> Sidekiq
+ PgBouncer --> Consul(Consul)
+ PostgreSQL --> Consul
+ PgBouncer --> PostgreSQL
+ NGINX --> Registry(Registry)
+ Unicorn --> Registry
+ NGINX --> Mattermost(Mattermost)
+ Mattermost --- Unicorn
+ Prometheus --> Alertmanager
+ Migrations --> PostgreSQL
+ Runner(Runner) --> NGINX
+ Unicorn -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 9200 --> ElasticSearch
+ Sidekiq -- TCP 80, 443 --> Sentry
+ Unicorn -- TCP 80, 443 --> Sentry
+ Sidekiq -- UDP 6831 --> Jaeger
+ Unicorn -- UDP 6831 --> Jaeger
+ Gitaly -- UDP 6831 --> Jaeger
+ GitLabShell -- UDP 6831 --> Jaeger
+ GitLabWorkhorse -- UDP 6831 --> Jaeger
+ Alertmanager -- TCP 25 --> SMTP
+ Sidekiq -- TCP 25 --> SMTP
+ Unicorn -- TCP 25 --> SMTP
+ Unicorn -- TCP 369 --> LDAP
+ Sidekiq -- TCP 369 --> LDAP
+ Unicorn -- TCP 443 --> ObjectStorage("Object Storage")
+ Sidekiq -- TCP 443 --> ObjectStorage
+ GitLabWorkhorse -- TCP 443 --> ObjectStorage
+ Registry -- TCP 443 --> ObjectStorage
+ Geo -- TCP 5432 --> PostgreSQL
+ end
+
+ HTTPK8s(HTTP/HTTPS) -- TCP 80, 443 --> LoadBalancerK8s(Load Balancer)
+ LoadBalancerK8s -- TCP 80, 443 --> nginx-ingressK8s
+ subgraph Kubernetes
+ PrometheusK8s(Prometheus)
+ TillerK8s(Tiller)
+ nginx-ingressK8s(NGINX Ingress)
+ Cert-ManagerK8s(Cert-Manager)
+ GitLabRunnerK8s(GitLab Runner)
+ GitLabRunnerK8s --> NGINX
+ JupyterHubK8s(JupyterHub)
+ nginx-ingressK8s --> JupyterHubK8s
+ KnativeK8s(Knative)
+ end
+
+classDef defaultoff stroke-dasharray: 5, 5
+class ElasticSearch defaultoff
+class Grafana defaultoff
+class PrometheusK8s defaultoff
+class TillerK8s defaultoff
+class nginx-ingressK8s defaultoff
+class Cert-ManagerK8s defaultoff
+class GitLabRunnerK8s defaultoff
+class JupyterHubK8s defaultoff
+class KnativeK8s defaultoff
+class HTTPK8s defaultoff
+class LoadBalancerK8s defaultoff
+class Sentry defaultoff
+class Jaeger defaultoff
+class Alertmanager defaultoff
+class SMTP defaultoff
+class ObjectStorage defaultoff
+class Geo defaultoff
+```
-_[edit diagram (for GitLab team members only)](https://docs.google.com/drawings/d/1fBzAyklyveF-i-2q-OHUIqDkYfjjxC4mq5shwKSZHLs/edit)_
+**Legend**:
+
+* ✅ - Automatically configured
+* ⚙ - Requires additional configuration
+* ⤓ - Additional software/service required
+* ❌ - Not available
+
+| Component | Description | [Omnibus GitLab](https://docs.gitlab.com/omnibus/README.html) | [GitLab chart](https://docs.gitlab.com/charts/) | [Minikube Minimal](https://docs.gitlab.com/charts/development/minikube/#deploying-gitlab-with-minimal-settings) | [GitLab.com](https://gitlab.com) | CE/EE |
+| --------- | ----------- |:--------------------:|:------------------:|:-----:|:--------:|:--------:|
+| NGINX | Routes requests to appropriate components, terminates SSL | [✅](https://docs.gitlab.com/omnibus/settings/nginx.html) | [✅](https://docs.gitlab.com/charts/charts/nginx/index.html) | [⚙](https://docs.gitlab.com/charts/charts/nginx/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) |CE & EE |
+| Unicorn (GitLab Rails) | Handles requests for the web interface and API | [✅](https://docs.gitlab.com/omnibus/settings/unicorn.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#unicorn) | CE & EE |
+| Sidekiq | Background jobs processor | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/sidekiq/index.html) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#sidekiq) | CE & EE |
+| Gitaly | Git RPC service for handling all git calls made by GitLab | [✅](https://docs.gitlab.com/ee/administration/gitaly/) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitaly/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Workhorse | Smart reverse proxy, handles large HTTP requests | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/unicorn/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Shell | Handles `git` over SSH sessions | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/gitlab-shell/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| GitLab Pages | Hosts static websites | [⚙](https://docs.gitlab.com/ee/administration/pages/) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [❌](https://gitlab.com/charts/gitlab/issues/37) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#gitlab-pages) | CE & EE |
+| Registry | Container registry, allows pushing and pulling of images | [⚙](https://docs.gitlab.com/ee/administration/container_registry.html#container-registry-domain-configuration) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/charts/charts/registry/index.html) | [✅](https://docs.gitlab.com/ee/user/project/container_registry.html#build-and-push-images) | CE & EE |
+| Redis | Caching service | [✅](https://docs.gitlab.com/omnibus/settings/redis.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#service-architecture) | CE & EE |
+| PostgreSQL | Database | [✅](https://docs.gitlab.com/omnibus/settings/database.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#postgresql) | CE & EE |
+| PgBouncer | Database connection pooling, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/pgbouncer.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | EE Only |
+| Consul | Database node discovery, failover | [⚙](https://docs.gitlab.com/ee/administration/high_availability/consul.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#consul) | EE Only |
+| Prometheus | Time-series database, metrics collection, and query service | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [⚙](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#prometheus) | CE & EE |
+| Prometheus Alertmanager | Deduplicates, groups, and routes alerts from Prometheus | [✅](https://gitlab.com/gitlab-org/omnibus-gitlab/blob/master/files/gitlab-config-template/gitlab.rb.template) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://github.com/helm/charts/tree/master/stable/prometheus) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| Grafana | Metrics dashboard | [⚙](https://docs.gitlab.com/ee/administration/monitoring/performance/grafana_configuration.html) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [⤓](https://github.com/helm/charts/tree/master/stable/grafana) | [✅](https://dashboards.gitlab.com/d/RZmbBr7mk/gitlab-triage?refresh=30s) | CE & EE |
+| Redis Exporter | Prometheus endpoint with Redis metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://docs.gitlab.com/charts/charts/redis/index.html) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| PostgreSQL Exporter | Prometheus endpoint with PostgreSQL metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://github.com/helm/charts/tree/master/stable/postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| PgBouncer Exporter | Prometheus endpoint with PgBouncer metrics | [⚙](https://docs.gitlab.com/ee/administration/monitoring/prometheus/pgbouncer_exporter.html) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [❌](https://docs.gitlab.com/charts/installation/deployment.html#postgresql) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| GitLab Monitor | Tracks a variety of GitLab metrics | [✅](https://docs.gitlab.com/ee/administration/monitoring/prometheus/gitlab_monitor_exporter.html) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [❌](https://gitlab.com/charts/gitlab/issues/319) | [✅](https://about.gitlab.com/handbook/engineering/monitoring/) | CE & EE |
+| Mattermost | Open-source Slack alternative | [⚙](https://docs.gitlab.com/omnibus/gitlab-mattermost/) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.mattermost.com/install/install-mmte-helm-gitlab-helm.html) | [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost_slash_commands.html#manual-configuration), [⤓](https://docs.gitlab.com/ee/user/project/integrations/mattermost.html) | CE & EE |
+| Minio | Object storage service | [⤓](https://min.io/download) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [✅](https://docs.gitlab.com/charts/charts/minio/index.html) | [❌](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#storage-architecture) | CE & EE |
+| Runner | Executes GitLab CI jobs | [⤓](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/runner/) | [⚙](https://docs.gitlab.com/runner/) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#shared-runners) | CE & EE |
+| Migrations | Database migrations | [✅](https://docs.gitlab.com/omnibus/settings/database.html#disabling-automatic-database-migration) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://docs.gitlab.com/charts/charts/gitlab/migrations/index.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#database-architecture) | CE & EE |
+| Certificate Management | TLS Settings, Let's Encrypt | [✅](https://docs.gitlab.com/omnibus/settings/ssl.html) | [✅](https://docs.gitlab.com/charts/installation/tls.html) | [⚙](https://docs.gitlab.com/charts/installation/tls.html) | [✅](https://about.gitlab.com/handbook/engineering/infrastructure/production-architecture/#secrets-management) | CE & EE |
+| GitLab Geo Node | Geographically distributed GitLab nodes | [⚙](https://docs.gitlab.com/ee/administration/geo/replication/index.html#setup-instructions) | [❌](https://gitlab.com/charts/gitlab/issues/8) | [❌](https://gitlab.com/charts/gitlab/issues/8) | ✅ | EE Only |
+| LDAP Authentication | Authenticate users against centralized LDAP directory | [⤓](https://docs.gitlab.com/ee/administration/auth/ldap.html) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [⤓](https://docs.gitlab.com/charts/charts/globals.html#ldap) | [❌](https://about.gitlab.com/pricing/#gitlab-com) | CE & EE |
+| Outbound email (SMTP) | Send email messages to users | [⤓](https://docs.gitlab.com/omnibus/settings/smtp.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#outgoing-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| Inbound email (SMTP) | Receive messages to update issues | [⤓](https://docs.gitlab.com/ee/administration/incoming_email.html) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [⤓](https://docs.gitlab.com/charts/installation/command-line-options.html#incoming-email-configuration) | [✅](https://docs.gitlab.com/ee/user/gitlab_com/#mail-configuration) | CE & EE |
+| ElasticSearch | Improved search within GitLab | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [⤓](https://docs.gitlab.com/ee/integration/elasticsearch.html) | [❌](https://gitlab.com/groups/gitlab-org/-/epics/153) | EE Only |
+| Sentry: GitLab instance | Tracking errors generated by the GitLab instance | [⤓](https://docs.gitlab.com/omnibus/settings/configuration.html#error-reporting-and-logging-with-sentry) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [❌](https://gitlab.com/charts/gitlab/issues/1319) | [✅](https://about.gitlab.com/handbook/support/workflows/services/gitlab_com/500_errors.html#searching-sentry) | CE & EE |
+| Jaeger: GitLab instance | View traces generated by the GitLab instance | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/charts/gitlab/issues/1320) | [❌](https://gitlab.com/gitlab-org/omnibus-gitlab/issues/4104) | CE & EE |
+| Sentry: deployed apps | Error tracking for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/error_tracking.html) | CE & EE |
+| Jaeger: deployed apps | Distributed tracing for deployed apps | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | [⤓](https://docs.gitlab.com/ee/user/project/operations/tracing.html) | EE Only |
+| Kubernetes cluster apps | Deploy [Helm](https://docs.helm.sh/), [Ingress](https://kubernetes.io/docs/concepts/services-networking/ingress/), [Cert-Manager](https://docs.cert-manager.io/en/latest/), [Prometheus](https://prometheus.io/docs/introduction/overview/), a [Runner](https://docs.gitlab.com/runner/), [JupyterHub](http://jupyter.org/), [Knative](https://cloud.google.com/knative) to a cluster | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | [⤓](https://docs.gitlab.com/ee/user/project/clusters/#installing-applications) | CE & EE |
A typical install of GitLab will be on GNU/Linux. It uses Nginx or Apache as a web front end to proxypass the Unicorn web server. By default, communication between Unicorn and the front end is via a Unix domain socket but forwarding requests via TCP is also supported. The web front end accesses `/home/git/gitlab/public` bypassing the Unicorn server to serve static pages, uploads (e.g. avatar images or attachments), and precompiled assets. GitLab serves web pages and a [GitLab API](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc/api) using the Unicorn web server. It uses Sidekiq as a job queue which, in turn, uses redis as a non-persistent database backend for job information, meta data, and incoming jobs.
+We also support deploying GitLab on Kubernetes using our [gitlab Helm chart](https://docs.gitlab.com/charts/).
+
The GitLab web app uses MySQL or PostgreSQL for persistent database information (e.g. users, permissions, issues, other meta data). GitLab stores the bare git repositories it serves in `/home/git/repositories` by default. It also keeps default branch and hook information with the bare repository.
When serving repositories over HTTP/HTTPS GitLab utilizes the GitLab API to resolve authorization and access as well as serving git objects.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index 60a8ffacd76..c694f0ed691 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -111,7 +111,7 @@ sudo apt-get install -y libcurl4-openssl-dev libexpat1-dev gettext libz-dev libs
# Download and compile from source
cd /tmp
curl --remote-name --location --progress https://www.kernel.org/pub/software/scm/git/git-2.21.0.tar.gz
-echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee' git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz
+echo '85eca51c7404da75e353eba587f87fea9481ba41e162206a6f70ad8118147bee git-2.21.0.tar.gz' | shasum -a256 -c - && tar -xzf git-2.21.0.tar.gz
cd git-2.21.0/
./configure
make prefix=/usr/local all
@@ -579,10 +579,10 @@ sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production force=yes
```
NOTE: **Note:**
-You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one), wait to expose GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login, you'll be forced to change the default password.
+You can set the Administrator/root password and e-mail by supplying them in environmental variables, `GITLAB_ROOT_PASSWORD` and `GITLAB_ROOT_EMAIL` respectively, as seen below. If you don't set the password (and it is set to the default one), wait to expose GitLab to the public internet until the installation is done and you've logged into the server the first time. During the first login, you'll be forced to change the default password. An Enterprise Edition license may also be installed at this time by supplying a full path in the `GITLAB_LICENSE_FILE` environment variable.
```sh
-sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail
+sudo -u git -H bundle exec rake gitlab:setup RAILS_ENV=production GITLAB_ROOT_PASSWORD=yourpassword GITLAB_ROOT_EMAIL=youremail GITLAB_LICENSE_FILE="/path/to/license"
```
### Secure secrets.yml
diff --git a/doc/install/requirements.md b/doc/install/requirements.md
index 17099c1d051..672723aaf12 100644
--- a/doc/install/requirements.md
+++ b/doc/install/requirements.md
@@ -87,7 +87,7 @@ if your available memory changes. We also recommend [configuring the kernel's sw
to a low value like `10` to make the most of your RAM while still having the swap
available when needed.
-Notice: The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
+NOTE: **Note:** The 25 workers of Sidekiq will show up as separate processes in your process overview (such as `top` or `htop`) but they share the same RAM allocation since Sidekiq is a multithreaded application. Please see the section below about Unicorn workers for information about how many you need of those.
## Database
@@ -224,5 +224,5 @@ Support is only provided for the current minor version of the major version you
Each time a new browser version is released, we begin supporting that version and stop supporting the third most recent version.
-Note: We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
+NOTE: **Note:** We do not support running GitLab with JavaScript disabled in the browser and have no plans of supporting that
in the future because we have features such as Issue Boards which require JavaScript extensively.
diff --git a/doc/integration/img/salesforce_app_details.png b/doc/integration/img/salesforce_app_details.png
new file mode 100644
index 00000000000..00e66f07282
--- /dev/null
+++ b/doc/integration/img/salesforce_app_details.png
Binary files differ
diff --git a/doc/integration/img/salesforce_app_secret_details.png b/doc/integration/img/salesforce_app_secret_details.png
new file mode 100644
index 00000000000..fad2a4a1f97
--- /dev/null
+++ b/doc/integration/img/salesforce_app_secret_details.png
Binary files differ
diff --git a/doc/integration/img/salesforce_oauth_app_details.png b/doc/integration/img/salesforce_oauth_app_details.png
new file mode 100644
index 00000000000..a5fb680cca6
--- /dev/null
+++ b/doc/integration/img/salesforce_oauth_app_details.png
Binary files differ
diff --git a/doc/integration/omniauth.md b/doc/integration/omniauth.md
index ef1f2df77f8..a13e9f73f48 100644
--- a/doc/integration/omniauth.md
+++ b/doc/integration/omniauth.md
@@ -35,6 +35,7 @@ contains some settings that are common for all providers.
- [JWT](../administration/auth/jwt.md)
- [OpenID Connect](../administration/auth/oidc.md)
- [UltraAuth](ultra_auth.md)
+- [SalesForce](salesforce.md)
## Initial OmniAuth Configuration
diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md
new file mode 100644
index 00000000000..18d42486fd6
--- /dev/null
+++ b/doc/integration/salesforce.md
@@ -0,0 +1,79 @@
+# SalesForce OmniAuth Provider
+
+You can integrate your GitLab instance with [SalesForce](https://www.salesforce.com/) to enable users to login to your GitLab instance with their SalesForce account.
+
+## Create SalesForce Application
+
+To enable SalesForce OmniAuth provider, you must use SalesForce's credentials for your GitLab instance.
+To get the credentials (a pair of Client ID and Client Secret), you must register an application on UltraAuth.
+
+1. Sign in to [SalesForce](https://www.salesforce.com/).
+
+1. Navigate to **Platform Tools/Apps** and click on **New Connected App**.
+
+1. Fill in the application details into the following fields:
+ - **Connected App Name** and **API Name**: Set to any value but consider something like `<Organization>'s GitLab`, `<Your Name>'s GitLab`, or something else that is descriptive.
+ - **Description**: Description for the application.
+
+ ![SalesForce App Details](img/salesforce_app_details.png)
+1. Select **API (Enable OAuth Settings)** and click on **Enable OAuth Settings**.
+1. Fill in the application details into the following fields:
+ - **Callback URL**: The call callback URL. For example, `https://gitlab.example.com/users/auth/salesforce/callback`.
+ - **Selected OAuth Scopes**: Move **Access your basic information (id, profile, email, address, phone)** and **Allow access to your unique identifier (openid)** to the right column.
+
+ ![SalesForce Oauth App Details](img/salesforce_oauth_app_details.png)
+1. Click **Save**.
+
+1. On your GitLab server, open the configuration file.
+
+ For omnibus package:
+
+ ```sh
+ sudo editor /etc/gitlab/gitlab.rb
+ ```
+
+ For installations from source:
+
+ ```sh
+ cd /home/git/gitlab
+ sudo -u git -H editor config/gitlab.yml
+ ```
+
+1. See [Initial OmniAuth Configuration](omniauth.md#initial-omniauth-configuration) for initial settings.
+
+1. Add the provider configuration:
+
+ For omnibus package:
+
+ ```ruby
+ gitlab_rails['omniauth_providers'] = [
+ {
+ "name" => "salesforce",
+ "app_id" => "SALESFORCE_CLIENT_ID",
+ "app_secret" => "SALESFORCE_CLIENT_SECRET"
+ }
+ ]
+ ```
+
+ For installation from source:
+
+ ```
+ - { name: 'salesforce',
+ app_id: 'SALESFORCE_CLIENT_ID',
+ app_secret: 'SALESFORCE_CLIENT_SECRET'
+ }
+ ```
+1. Change `SALESFORCE_CLIENT_ID` to the Consumer Key from the SalesForce connected application page.
+1. Change `SALESFORCE_CLIENT_SECRET` to the Client Secret from the SalesForce connected application page.
+ ![SalesForce App Secret Details](img/salesforce_app_secret_details.png)
+
+1. Save the configuration file.
+1. [Reconfigure GitLab]( ../administration/restart_gitlab.md#omnibus-gitlab-reconfigure ) or [restart GitLab]( ../administration/restart_gitlab.md#installations-from-source ) for the changes to take effect if you
+ installed GitLab via Omnibus or from source respectively.
+
+On the sign in page, there should now be a SalesForce icon below the regular sign in form.
+Click the icon to begin the authentication process. SalesForce will ask the user to sign in and authorize the GitLab application.
+If everything goes well, the user will be returned to GitLab and will be signed in.
+
+NOTE: **Note:**
+GitLab requires the email address of each new user. Once the user is logged in using SalesForce, GitLab will redirect the user to the profile page where they will have to provide the email and verify the email.
diff --git a/doc/update/mysql_to_postgresql.md b/doc/update/mysql_to_postgresql.md
index e607dbceeea..b83abcd36f7 100644
--- a/doc/update/mysql_to_postgresql.md
+++ b/doc/update/mysql_to_postgresql.md
@@ -45,6 +45,22 @@ For other distributions, follow the instructions in PostgreSQL's
[download page](https://www.postgresql.org/download/) to add their repository
and then install `pgloader`.
+If you are migrating to a Docker based installation, you will need to install
+pgloader within the container as it is not included in the container image.
+
+1. Start a shell session in the context of the running container:
+
+ ``` bash
+ docker exec -it gitlab bash
+ ```
+
+1. Install pgloader:
+
+ ``` bash
+ apt-get update
+ apt-get -y install pgloader
+ ```
+
## Omnibus GitLab installations
For [Omnibus GitLab packages](https://about.gitlab.com/install/), you'll first
diff --git a/doc/user/admin_area/license.md b/doc/user/admin_area/license.md
index 45f986d480f..49959a9daef 100644
--- a/doc/user/admin_area/license.md
+++ b/doc/user/admin_area/license.md
@@ -2,7 +2,8 @@
To activate all GitLab Enterprise Edition (EE) functionality, you need to upload
a license. Once you've received your license from GitLab Inc., you can upload it
-by **signing into your GitLab instance as an admin**.
+by **signing into your GitLab instance as an admin** or add it at
+installation time.
The license has the form of a base64 encoded ASCII text with a `.gitlab-license`
extension and can be obtained when you [purchase one][pricing] or when you sign
@@ -42,6 +43,36 @@ Otherwise, you can:
"Enter license key" option, copy the license, paste it into the "License key"
field and click **Upload license**.
+## Add your license at install time
+
+The license may be automatically injected during installation using one of
+two methods.
+
+The first requires a license file named `Gitlab.gitlab-release`.
+
+Place it in the `config/` directory if installing from source or in the
+`/etc/gitlab/` directory if installing Omnibus.
+
+The second allows the administrator to configure the location and
+filename of the license.
+
+Source installations should set the `GITLAB_LICENSE_FILE` environment
+variable with the path to a valid GitLab Enterprise Edition license.
+
+```sh
+export GITLAB_LICENSE_FILE="/path/to/license/file"
+```
+
+Omnibus installations should add this entry to `gitlab.rb`:
+
+```ruby
+gitlab_rails['license_file'] = "/path/to/license/file"
+```
+
+CAUTION:: **Caution:**
+These methods will only add a license at the time of installation. Use the
+admin area in the web ui to renew or upgrade licenses.
+
---
Once the license is uploaded, all GitLab Enterprise Edition functionality
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index e165a120162..8b5d80efb0d 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -48,6 +48,8 @@ You can view the exact JSON payload in the administration panel. To view the pay
1. Expand **Settings** in the left sidebar and click on **Metrics and profiling**.
1. Expand **Usage statistics** and click on the **Preview payload** button.
+You can see how [the usage ping data maps to different stages of the product](https://gitlab.com/gitlab-data/analytics/blob/master/transform/snowflake-dbt/data/ping_metrics_to_stage_mapping_data.csv).
+
### Deactivate the usage ping
The usage ping is opt-out. If you want to deactivate this feature, go to
diff --git a/doc/user/discussions/index.md b/doc/user/discussions/index.md
index 248f8395db1..5d69efc3600 100644
--- a/doc/user/discussions/index.md
+++ b/doc/user/discussions/index.md
@@ -385,11 +385,6 @@ the Merge Request authored by the user that applied them.
![Add a new comment](img/insert_suggestion.png)
- > **Note:**
- The suggestion will only affect the commented line. Multi-line
- suggestions are currently not supported. Will be introduced by
- [#53310](https://gitlab.com/gitlab-org/gitlab-ce/issues/53310).
-
1. In the comment, add your suggestion to the pre-populated code block:
![Add a suggestion into a code block tagged properly](img/make_suggestion.png)
@@ -401,13 +396,10 @@ the Merge Request authored by the user that applied them.
![Apply suggestions](img/suggestion.png)
- > **Note:**
- Discussions are _not_ automatically resolved. Will be introduced by
- [#54405](https://gitlab.com/gitlab-org/gitlab-ce/issues/54405).
-
Once the author applies a suggestion, it will be marked with the **Applied** label,
-and GitLab will create a new commit with the message `Apply suggestion to <file-name>`
-and push the suggested change directly into the codebase in the merge request's branch.
+the discussion will be automatically resolved, and GitLab will create a new commit
+with the message `Apply suggestion to <file-name>` and push the suggested change
+directly into the codebase in the merge request's branch.
[Developer permission](../permissions.md) is required to do so.
> **Note:**
diff --git a/lib/api/helpers/related_resources_helpers.rb b/lib/api/helpers/related_resources_helpers.rb
index 793ae11b41d..9cdde25fe4e 100644
--- a/lib/api/helpers/related_resources_helpers.rb
+++ b/lib/api/helpers/related_resources_helpers.rb
@@ -13,6 +13,10 @@ module API
available?(:merge_requests, project, options[:current_user])
end
+ def expose_path(path)
+ Gitlab::Utils.append_path(Gitlab.config.gitlab.relative_url_root, path)
+ end
+
def expose_url(path)
url_options = Gitlab::Application.routes.default_url_options
protocol, host, port, script_name = url_options.values_at(:protocol, :host, :port, :script_name)
diff --git a/lib/api/merge_requests.rb b/lib/api/merge_requests.rb
index ce85772e4ed..daa98c22e5e 100644
--- a/lib/api/merge_requests.rb
+++ b/lib/api/merge_requests.rb
@@ -367,6 +367,10 @@ module API
merge_request = find_project_merge_request(params[:merge_request_iid])
merge_when_pipeline_succeeds = to_boolean(params[:merge_when_pipeline_succeeds])
+ if merge_when_pipeline_succeeds || merge_request.merge_when_pipeline_succeeds
+ render_api_error!('Not allowed: pipeline does not exist', 405) unless merge_request.head_pipeline
+ end
+
# Merge request can not be merged
# because user dont have permissions to push into target branch
unauthorized! unless merge_request.can_be_merged_by?(current_user)
diff --git a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
index eef361c19e9..324e39c7747 100644
--- a/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Security/Container-Scanning.gitlab-ci.yml
@@ -22,7 +22,7 @@ container_scanning:
DOCKER_SERVICE: docker
DOCKER_HOST: tcp://${DOCKER_SERVICE}:2375/
# https://hub.docker.com/r/arminc/clair-local-scan/tags
- CLAIR_LOCAL_SCAN_VERSION: v2.0.6
+ CLAIR_LOCAL_SCAN_VERSION: v2.0.8_fe9b059d930314b54c78f75afe265955faf4fdc1
allow_failure: true
services:
- docker:stable-dind
diff --git a/lib/gitlab/database/migration_helpers.rb b/lib/gitlab/database/migration_helpers.rb
index 7f5eb1188fc..cc61bb7fa02 100644
--- a/lib/gitlab/database/migration_helpers.rb
+++ b/lib/gitlab/database/migration_helpers.rb
@@ -905,6 +905,12 @@ module Gitlab
end
end
+ def remove_foreign_key_if_exists(*args)
+ if foreign_key_exists?(*args)
+ remove_foreign_key(*args)
+ end
+ end
+
def remove_foreign_key_without_error(*args)
remove_foreign_key(*args)
rescue ArgumentError
diff --git a/lib/gitlab/gitaly_client.rb b/lib/gitlab/gitaly_client.rb
index 4908f236cd1..05e06eec012 100644
--- a/lib/gitlab/gitaly_client.rb
+++ b/lib/gitlab/gitaly_client.rb
@@ -32,7 +32,8 @@ module Gitlab
CLIENT_NAME = (Sidekiq.server? ? 'gitlab-sidekiq' : 'gitlab-web').freeze
SERVER_FEATURE_CATFILE_CACHE = 'catfile-cache'.freeze
- SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE].freeze
+ # Server feature flags should use '_' to separate words.
+ SERVER_FEATURE_FLAGS = [SERVER_FEATURE_CATFILE_CACHE, 'delta_islands'].freeze
MUTEX = Mutex.new
diff --git a/lib/gitlab/group_search_results.rb b/lib/gitlab/group_search_results.rb
index 7255293b194..334642f252e 100644
--- a/lib/gitlab/group_search_results.rb
+++ b/lib/gitlab/group_search_results.rb
@@ -2,6 +2,8 @@
module Gitlab
class GroupSearchResults < SearchResults
+ attr_reader :group
+
def initialize(current_user, limit_projects, group, query, default_project_filter: false, per_page: 20)
super(current_user, limit_projects, query, default_project_filter: default_project_filter, per_page: per_page)
@@ -26,5 +28,9 @@ module Gitlab
.where(id: groups.select('members.user_id'))
end
# rubocop:enable CodeReuse/ActiveRecord
+
+ def issuable_params
+ super.merge(group_id: group.id)
+ end
end
end
diff --git a/lib/gitlab/metrics/samplers/ruby_sampler.rb b/lib/gitlab/metrics/samplers/ruby_sampler.rb
index 18a69321905..4d9c43f37e7 100644
--- a/lib/gitlab/metrics/samplers/ruby_sampler.rb
+++ b/lib/gitlab/metrics/samplers/ruby_sampler.rb
@@ -23,25 +23,32 @@ module Gitlab
end
def init_metrics
- metrics = {}
- metrics[:sampler_duration] = ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels)
- metrics[:total_time] = ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ metrics = {
+ file_descriptors: ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum),
+ memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum),
+ process_cpu_seconds_total: ::Gitlab::Metrics.gauge(with_prefix(:process, :cpu_seconds_total), 'Process CPU seconds total'),
+ process_max_fds: ::Gitlab::Metrics.gauge(with_prefix(:process, :max_fds), 'Process max fds'),
+ process_resident_memory_bytes: ::Gitlab::Metrics.gauge(with_prefix(:process, :resident_memory_bytes), 'Memory used', labels, :livesum),
+ process_start_time_seconds: ::Gitlab::Metrics.gauge(with_prefix(:process, :start_time_seconds), 'Process start time seconds'),
+ sampler_duration: ::Gitlab::Metrics.counter(with_prefix(:sampler, :duration_seconds_total), 'Sampler time', labels),
+ total_time: ::Gitlab::Metrics.counter(with_prefix(:gc, :duration_seconds_total), 'Total GC time', labels)
+ }
+
GC.stat.keys.each do |key|
metrics[key] = ::Gitlab::Metrics.gauge(with_prefix(:gc_stat, key), to_doc_string(key), labels, :livesum)
end
- metrics[:memory_usage] = ::Gitlab::Metrics.gauge(with_prefix(:memory, :bytes), 'Memory used', labels, :livesum)
- metrics[:file_descriptors] = ::Gitlab::Metrics.gauge(with_prefix(:file, :descriptors), 'File descriptors used', labels, :livesum)
-
metrics
end
def sample
start_time = System.monotonic_time
- metrics[:memory_usage].set(labels.merge(worker_label), System.memory_usage)
metrics[:file_descriptors].set(labels.merge(worker_label), System.file_descriptor_count)
-
+ metrics[:process_cpu_seconds_total].set(labels.merge(worker_label), ::Gitlab::Metrics::System.cpu_time)
+ metrics[:process_max_fds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.max_open_file_descriptors)
+ metrics[:process_start_time_seconds].set(labels.merge(worker_label), ::Gitlab::Metrics::System.process_start_time)
+ set_memory_usage_metrics
sample_gc
metrics[:sampler_duration].increment(labels, System.monotonic_time - start_time)
@@ -61,6 +68,14 @@ module Gitlab
metrics[:total_time].increment(labels, GC::Profiler.total_time)
end
+ def set_memory_usage_metrics
+ memory_usage = System.memory_usage
+ memory_labels = labels.merge(worker_label)
+
+ metrics[:memory_bytes].set(memory_labels, memory_usage)
+ metrics[:process_resident_memory_bytes].set(memory_labels, memory_usage)
+ end
+
def worker_label
return {} unless defined?(Unicorn::Worker)
diff --git a/lib/gitlab/metrics/samplers/unicorn_sampler.rb b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
index bec64e864b3..1b6c52ac0bf 100644
--- a/lib/gitlab/metrics/samplers/unicorn_sampler.rb
+++ b/lib/gitlab/metrics/samplers/unicorn_sampler.rb
@@ -8,12 +8,16 @@ module Gitlab
super(interval)
end
- def unicorn_active_connections
- @unicorn_active_connections ||= ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max)
+ def metrics
+ @metrics ||= init_metrics
end
- def unicorn_queued_connections
- @unicorn_queued_connections ||= ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max)
+ def init_metrics
+ {
+ unicorn_active_connections: ::Gitlab::Metrics.gauge(:unicorn_active_connections, 'Unicorn active connections', {}, :max),
+ unicorn_queued_connections: ::Gitlab::Metrics.gauge(:unicorn_queued_connections, 'Unicorn queued connections', {}, :max),
+ unicorn_workers: ::Gitlab::Metrics.gauge(:unicorn_workers, 'Unicorn workers')
+ }
end
def enabled?
@@ -23,14 +27,13 @@ module Gitlab
def sample
Raindrops::Linux.tcp_listener_stats(tcp_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'tcp', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('tcp', addr, stats)
end
-
Raindrops::Linux.unix_listener_stats(unix_listeners).each do |addr, stats|
- unicorn_active_connections.set({ socket_type: 'unix', socket_address: addr }, stats.active)
- unicorn_queued_connections.set({ socket_type: 'unix', socket_address: addr }, stats.queued)
+ set_unicorn_connection_metrics('unix', addr, stats)
end
+
+ metrics[:unicorn_workers].set({}, unicorn_workers_count)
end
private
@@ -39,6 +42,13 @@ module Gitlab
@tcp_listeners ||= Unicorn.listener_names.grep(%r{\A[^/]+:\d+\z})
end
+ def set_unicorn_connection_metrics(type, addr, stats)
+ labels = { socket_type: type, socket_address: addr }
+
+ metrics[:unicorn_active_connections].set(labels, stats.active)
+ metrics[:unicorn_queued_connections].set(labels, stats.queued)
+ end
+
def unix_listeners
@unix_listeners ||= Unicorn.listener_names - tcp_listeners
end
@@ -46,6 +56,10 @@ module Gitlab
def unicorn_with_listeners?
defined?(Unicorn) && Unicorn.listener_names.any?
end
+
+ def unicorn_workers_count
+ Sys::ProcTable.ps.select {|p| p.cmdline.match(/unicorn_rails worker.+ #{Rails.root.to_s}/)}.count
+ end
end
end
end
diff --git a/lib/gitlab/metrics/system.rb b/lib/gitlab/metrics/system.rb
index 426496855e3..ce0bb82d138 100644
--- a/lib/gitlab/metrics/system.rb
+++ b/lib/gitlab/metrics/system.rb
@@ -23,6 +23,21 @@ module Gitlab
def self.file_descriptor_count
Dir.glob('/proc/self/fd/*').length
end
+
+ def self.max_open_file_descriptors
+ match = File.read('/proc/self/limits').match(/Max open files\s*(\d+)/)
+
+ return unless match && match[1]
+
+ match[1].to_i
+ end
+
+ def self.process_start_time
+ start_time_in_jiffies = Sys::ProcTable.ps(pid: Process.pid).starttime
+ return 0 unless start_time_in_jiffies
+
+ start_time_in_jiffies / 100
+ end
else
def self.memory_usage
0.0
@@ -31,6 +46,14 @@ module Gitlab
def self.file_descriptor_count
0
end
+
+ def self.max_open_file_descriptors
+ 0
+ end
+
+ def self.process_start_time
+ 0
+ end
end
# THREAD_CPUTIME is not supported on OS X
diff --git a/lib/gitlab/project_search_results.rb b/lib/gitlab/project_search_results.rb
index 58f06b6708c..78337518988 100644
--- a/lib/gitlab/project_search_results.rb
+++ b/lib/gitlab/project_search_results.rb
@@ -145,5 +145,9 @@ module Gitlab
def repository_wiki_ref
@repository_wiki_ref ||= repository_ref || project.wiki.default_branch
end
+
+ def issuable_params
+ super.merge(project_id: project.id)
+ end
end
end
diff --git a/lib/gitlab/search_results.rb b/lib/gitlab/search_results.rb
index a29517e068f..4a097a00101 100644
--- a/lib/gitlab/search_results.rb
+++ b/lib/gitlab/search_results.rb
@@ -2,6 +2,8 @@
module Gitlab
class SearchResults
+ COUNT_LIMIT = 1001
+
attr_reader :current_user, :query, :per_page
# Limit search results by passed projects
@@ -25,29 +27,26 @@ module Gitlab
def objects(scope, page = nil, without_count = true)
collection = case scope
when 'projects'
- projects.page(page).per(per_page)
+ projects
when 'issues'
- issues.page(page).per(per_page)
+ issues
when 'merge_requests'
- merge_requests.page(page).per(per_page)
+ merge_requests
when 'milestones'
- milestones.page(page).per(per_page)
+ milestones
when 'users'
- users.page(page).per(per_page)
+ users
else
- Kaminari.paginate_array([]).page(page).per(per_page)
- end
+ Kaminari.paginate_array([])
+ end.page(page).per(per_page)
without_count ? collection.without_count : collection
end
- # rubocop: disable CodeReuse/ActiveRecord
def limited_projects_count
- @limited_projects_count ||= projects.limit(count_limit).count
+ @limited_projects_count ||= limited_count(projects)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_issues_count
return @limited_issues_count if @limited_issues_count
@@ -56,35 +55,28 @@ module Gitlab
# and confidential issues user has access to, is too complex.
# It's faster to try to fetch all public issues first, then only
# if necessary try to fetch all issues.
- sum = issues(public_only: true).limit(count_limit).count
- @limited_issues_count = sum < count_limit ? issues.limit(count_limit).count : sum
+ sum = limited_count(issues(public_only: true))
+ @limited_issues_count = sum < count_limit ? limited_count(issues) : sum
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_merge_requests_count
- @limited_merge_requests_count ||= merge_requests.limit(count_limit).count
+ @limited_merge_requests_count ||= limited_count(merge_requests)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def limited_milestones_count
- @limited_milestones_count ||= milestones.limit(count_limit).count
+ @limited_milestones_count ||= limited_count(milestones)
end
- # rubocop: enable CodeReuse/ActiveRecord
- # rubocop:disable CodeReuse/ActiveRecord
def limited_users_count
- @limited_users_count ||= users.limit(count_limit).count
+ @limited_users_count ||= limited_count(users)
end
- # rubocop:enable CodeReuse/ActiveRecord
def single_commit_result?
false
end
def count_limit
- 1001
+ COUNT_LIMIT
end
def users
@@ -99,23 +91,15 @@ module Gitlab
limit_projects.search(query)
end
- # rubocop: disable CodeReuse/ActiveRecord
def issues(finder_params = {})
- issues = IssuesFinder.new(current_user, finder_params).execute
+ issues = IssuesFinder.new(current_user, issuable_params.merge(finder_params)).execute
+
unless default_project_filter
- issues = issues.where(project_id: project_ids_relation)
+ issues = issues.where(project_id: project_ids_relation) # rubocop: disable CodeReuse/ActiveRecord
end
- issues =
- if query =~ /#(\d+)\z/
- issues.where(iid: $1)
- else
- issues.full_search(query)
- end
-
- issues.reorder('issues.updated_at DESC')
+ issues
end
- # rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
def milestones
@@ -125,23 +109,15 @@ module Gitlab
end
# rubocop: enable CodeReuse/ActiveRecord
- # rubocop: disable CodeReuse/ActiveRecord
def merge_requests
- merge_requests = MergeRequestsFinder.new(current_user).execute
+ merge_requests = MergeRequestsFinder.new(current_user, issuable_params).execute
+
unless default_project_filter
merge_requests = merge_requests.in_projects(project_ids_relation)
end
- merge_requests =
- if query =~ /[#!](\d+)\z/
- merge_requests.where(iid: $1)
- else
- merge_requests.full_search(query)
- end
-
- merge_requests.reorder('merge_requests.updated_at DESC')
+ merge_requests
end
- # rubocop: enable CodeReuse/ActiveRecord
def default_scope
'projects'
@@ -152,5 +128,23 @@ module Gitlab
limit_projects.select(:id).reorder(nil)
end
# rubocop: enable CodeReuse/ActiveRecord
+
+ def issuable_params
+ {}.tap do |params|
+ params[:sort] = 'updated_desc'
+
+ if query =~ /#(\d+)\z/
+ params[:iids] = $1
+ else
+ params[:search] = query
+ end
+ end
+ end
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ def limited_count(relation)
+ relation.reorder(nil).limit(count_limit).size
+ end
+ # rubocop: enable CodeReuse/ActiveRecord
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 5eaf87025d0..6a3184175f4 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -1027,6 +1027,9 @@ msgstr ""
msgid "Applying multiple commands"
msgstr ""
+msgid "Applying suggestion"
+msgstr ""
+
msgid "Apr"
msgstr ""
@@ -3416,6 +3419,9 @@ msgstr ""
msgid "Disabled"
msgstr ""
+msgid "Disabled mirrors can only be enabled by instance owners. It is recommended that you delete them."
+msgstr ""
+
msgid "Discard"
msgstr ""
@@ -4127,6 +4133,18 @@ msgstr ""
msgid "ExternalAuthorizationService|When no classification label is set the default label `%{default_label}` will be used."
msgstr ""
+msgid "ExternalMetrics|Add a button to the metrics dashboard linking directly to your existing external dashboards."
+msgstr ""
+
+msgid "ExternalMetrics|Enter the URL of the dashboard you want to link to"
+msgstr ""
+
+msgid "ExternalMetrics|External Dashboard"
+msgstr ""
+
+msgid "ExternalMetrics|Full dashboard URL"
+msgstr ""
+
msgid "ExternalWikiService|External Wiki"
msgstr ""
@@ -4193,6 +4211,9 @@ msgstr ""
msgid "Failed to remove user key."
msgstr ""
+msgid "Failed to save comment!"
+msgstr ""
+
msgid "Failed to save merge conflicts resolutions. Please try again!"
msgstr ""
@@ -4205,6 +4226,9 @@ msgstr ""
msgid "Failed to save preferences."
msgstr ""
+msgid "Failed to update branch!"
+msgstr ""
+
msgid "Failed to update issues, please try again."
msgstr ""
@@ -5905,6 +5929,9 @@ msgstr ""
msgid "Metrics for environment"
msgstr ""
+msgid "Metrics|Add metric"
+msgstr ""
+
msgid "Metrics|Check out the CI/CD documentation on deploying to an environment"
msgstr ""
@@ -6965,12 +6992,33 @@ msgstr ""
msgid "Preferences saved."
msgstr ""
+msgid "Preferences|Display time in 24-hour format"
+msgstr ""
+
+msgid "Preferences|For example: 30 mins ago."
+msgstr ""
+
msgid "Preferences|Navigation theme"
msgstr ""
+msgid "Preferences|These settings will update how dates and times are displayed for you."
+msgstr ""
+
msgid "Preferences|This feature is experimental and translations are not complete yet"
msgstr ""
+msgid "Preferences|Time display"
+msgstr ""
+
+msgid "Preferences|Time format"
+msgstr ""
+
+msgid "Preferences|Time preferences"
+msgstr ""
+
+msgid "Preferences|Use relative times"
+msgstr ""
+
msgid "Press %{key}-C to copy"
msgstr ""
@@ -7181,6 +7229,9 @@ msgstr ""
msgid "Profiles|This information will appear on your profile"
msgstr ""
+msgid "Profiles|Time settings"
+msgstr ""
+
msgid "Profiles|Two-Factor Authentication"
msgstr ""
@@ -7223,6 +7274,9 @@ msgstr ""
msgid "Profiles|You can change your avatar here or remove the current avatar to revert to %{gravatar_link}"
msgstr ""
+msgid "Profiles|You can set your current timezone here"
+msgstr ""
+
msgid "Profiles|You can upload your avatar here"
msgstr ""
@@ -7652,6 +7706,9 @@ msgstr ""
msgid "Protected"
msgstr ""
+msgid "Protected Branch"
+msgstr ""
+
msgid "Protected Tag"
msgstr ""
@@ -9586,6 +9643,9 @@ msgstr ""
msgid "This action can lead to data loss. To prevent accidental actions we ask you to confirm your intention."
msgstr ""
+msgid "This also resolves the discussion"
+msgstr ""
+
msgid "This application was created by %{link_to_owner}."
msgstr ""
@@ -10577,6 +10637,9 @@ msgstr ""
msgid "View file @ "
msgstr ""
+msgid "View full dashboard"
+msgstr ""
+
msgid "View group labels"
msgstr ""
diff --git a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
index a118176eb8a..15cd59f041b 100644
--- a/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
+++ b/qa/qa/specs/features/browser_ui/1_manage/login/login_via_oauth_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Manage', :orchestrated, :oauth do
+ # https://gitlab.com/gitlab-org/quality/nightly/issues/100
+ context 'Manage', :orchestrated, :oauth, :quarantine do
describe 'OAuth login' do
it 'User logs in to GitLab with GitHub OAuth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
index 9e48ee7ca2a..db33c6330ff 100644
--- a/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/merge_request/view_merge_request_diff_patch_spec.rb
@@ -1,7 +1,8 @@
# frozen_string_literal: true
module QA
- context 'Create' do
+ # https://gitlab.com/gitlab-org/quality/staging/issues/55
+ context 'Create', :quarantine do
describe 'Download merge request patch and diff' do
before(:context) do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/spec/factories/projects.rb b/spec/factories/projects.rb
index ab185ab3972..743ec322885 100644
--- a/spec/factories/projects.rb
+++ b/spec/factories/projects.rb
@@ -260,6 +260,7 @@ FactoryBot.define do
trait(:merge_requests_enabled) { merge_requests_access_level ProjectFeature::ENABLED }
trait(:merge_requests_disabled) { merge_requests_access_level ProjectFeature::DISABLED }
trait(:merge_requests_private) { merge_requests_access_level ProjectFeature::PRIVATE }
+ trait(:merge_requests_public) { merge_requests_access_level ProjectFeature::PUBLIC }
trait(:repository_enabled) { repository_access_level ProjectFeature::ENABLED }
trait(:repository_disabled) { repository_access_level ProjectFeature::DISABLED }
trait(:repository_private) { repository_access_level ProjectFeature::PRIVATE }
diff --git a/spec/features/dashboard/user_filters_projects_spec.rb b/spec/features/dashboard/user_filters_projects_spec.rb
index e0553086fd7..5b17c49db2d 100644
--- a/spec/features/dashboard/user_filters_projects_spec.rb
+++ b/spec/features/dashboard/user_filters_projects_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe 'Dashboard > User filters projects' do
let(:user) { create(:user) }
- let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace) }
+ let(:project) { create(:project, name: 'Victorialand', namespace: user.namespace, created_at: 2.seconds.ago, updated_at: 2.seconds.ago) }
let(:user2) { create(:user) }
- let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace) }
+ let(:project2) { create(:project, name: 'Treasure', namespace: user2.namespace, created_at: 1.second.ago, updated_at: 1.second.ago) }
before do
project.add_maintainer(user)
@@ -129,7 +129,7 @@ describe 'Dashboard > User filters projects' do
list = page.all('.projects-list .project-name').map(&:text)
- expect(list).to match(["Private project", "Treasure", "Victorialand"])
+ expect(list).to contain_exactly("Private project", "Treasure", "Victorialand")
end
it 'filters internal projects only' do
@@ -139,14 +139,14 @@ describe 'Dashboard > User filters projects' do
list = page.all('.projects-list .project-name').map(&:text)
- expect(list).to match(['Internal project'])
+ expect(list).to contain_exactly('Internal project')
end
it 'filters any project' do
select_dropdown_option '#filtered-search-visibility-dropdown', 'Any'
list = page.all('.projects-list .project-name').map(&:text)
- expect(list).to match(["Internal project", "Private project", "Treasure", "Victorialand"])
+ expect(list).to contain_exactly("Internal project", "Private project", "Treasure", "Victorialand")
end
end
diff --git a/spec/features/merge_request/user_sees_merge_widget_spec.rb b/spec/features/merge_request/user_sees_merge_widget_spec.rb
index 40ba676ff92..a32c6bdcf8f 100644
--- a/spec/features/merge_request/user_sees_merge_widget_spec.rb
+++ b/spec/features/merge_request/user_sees_merge_widget_spec.rb
@@ -670,4 +670,26 @@ describe 'Merge request > User sees merge widget', :js do
end
end
end
+
+ context 'when MR has pipeline but user does not have permission' do
+ let(:sha) { project.commit(merge_request.source_branch).sha }
+ let!(:pipeline) { create(:ci_pipeline_without_jobs, status: 'success', sha: sha, project: project, ref: merge_request.source_branch) }
+
+ before do
+ project.update(
+ visibility_level: Gitlab::VisibilityLevel::PUBLIC,
+ public_builds: false
+ )
+ merge_request.update!(head_pipeline: pipeline)
+ sign_out(:user)
+
+ visit project_merge_request_path(project, merge_request)
+ end
+
+ it 'renders a CI pipeline error' do
+ within '.ci-widget' do
+ expect(page).to have_content('Could not retrieve the pipeline status.')
+ end
+ end
+ end
end
diff --git a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
index 1b5dd6945e0..04c7f4b6c76 100644
--- a/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
+++ b/spec/features/merge_request/user_suggests_changes_on_diff_spec.rb
@@ -121,7 +121,7 @@ describe 'User comments on a diff', :js do
end
context 'multi-line suggestions' do
- it 'suggestion is presented' do
+ before do
click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
page.within('.js-discussion-note-form') do
@@ -130,7 +130,9 @@ describe 'User comments on a diff', :js do
end
wait_for_requests
+ end
+ it 'suggestion is presented' do
page.within('.diff-discussions') do
expect(page).to have_button('Apply suggestion')
expect(page).to have_content('Suggested change')
@@ -160,22 +162,24 @@ describe 'User comments on a diff', :js do
end
it 'suggestion is appliable' do
- click_diff_line(find("[id='#{sample_compare.changes[1][:line_code]}']"))
+ page.within('.diff-discussions') do
+ expect(page).not_to have_content('Applied')
- page.within('.js-discussion-note-form') do
- fill_in('note_note', with: "```suggestion:-3+5\n# change to a\n# comment\n# with\n# broken\n# lines\n```")
- click_button('Comment')
- end
+ click_button('Apply suggestion')
+ wait_for_requests
- wait_for_requests
+ expect(page).to have_content('Applied')
+ end
+ end
+ it 'resolves discussion when applied' do
page.within('.diff-discussions') do
- expect(page).not_to have_content('Applied')
+ expect(page).not_to have_content('Unresolve discussion')
click_button('Apply suggestion')
wait_for_requests
- expect(page).to have_content('Applied')
+ expect(page).to have_content('Unresolve discussion')
end
end
end
diff --git a/spec/features/oauth_login_spec.rb b/spec/features/oauth_login_spec.rb
index f4105730402..5ebfc32952d 100644
--- a/spec/features/oauth_login_spec.rb
+++ b/spec/features/oauth_login_spec.rb
@@ -14,7 +14,7 @@ describe 'OAuth Login', :js, :allow_forgery_protection do
end
providers = [:github, :twitter, :bitbucket, :gitlab, :google_oauth2,
- :facebook, :cas3, :auth0, :authentiq]
+ :facebook, :cas3, :auth0, :authentiq, :salesforce]
before(:all) do
# The OmniAuth `full_host` parameter doesn't get set correctly (it gets set to something like `http://localhost`
diff --git a/spec/features/profiles/user_edit_preferences_spec.rb b/spec/features/profiles/user_edit_preferences_spec.rb
new file mode 100644
index 00000000000..2d2da222998
--- /dev/null
+++ b/spec/features/profiles/user_edit_preferences_spec.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+require 'spec_helper'
+
+describe 'User edit preferences profile' do
+ let(:user) { create(:user) }
+
+ before do
+ stub_feature_flags(user_time_settings: true)
+ sign_in(user)
+ visit(profile_preferences_path)
+ end
+
+ it 'allows the user to toggle their time format preference' do
+ field = page.find_field("user[time_format_in_24h]")
+
+ expect(field).not_to be_checked
+
+ field.click
+
+ expect(field).to be_checked
+ end
+
+ it 'allows the user to toggle their time display preference' do
+ field = page.find_field("user[time_display_relative]")
+
+ expect(field).to be_checked
+
+ field.click
+
+ expect(field).not_to be_checked
+ end
+end
diff --git a/spec/features/profiles/user_edit_profile_spec.rb b/spec/features/profiles/user_edit_profile_spec.rb
index b43711f6ef6..a53da94ef7d 100644
--- a/spec/features/profiles/user_edit_profile_spec.rb
+++ b/spec/features/profiles/user_edit_profile_spec.rb
@@ -327,5 +327,37 @@ describe 'User edit profile' do
end
end
end
+
+ context 'User time preferences', :js do
+ let(:issue) { create(:issue, project: project)}
+ let(:project) { create(:project) }
+
+ before do
+ stub_feature_flags(user_time_settings: true)
+ end
+
+ it 'shows the user time preferences form' do
+ expect(page).to have_content('Time settings')
+ end
+
+ it 'allows the user to select a time zone from a dropdown list of options' do
+ expect(page.find('.user-time-preferences .dropdown')).not_to have_css('.show')
+
+ page.find('.user-time-preferences .js-timezone-dropdown').click
+
+ expect(page.find('.user-time-preferences .dropdown')).to have_css('.show')
+
+ page.find("a", text: "Nuku'alofa").click
+
+ tz = page.find('.user-time-preferences #user_timezone', visible: false)
+
+ expect(tz.value).to eq('Pacific/Tongatapu')
+ end
+
+ it 'timezone defaults to servers default' do
+ timezone_name = Time.zone.tzinfo.name
+ expect(page.find('.user-time-preferences #user_timezone', visible: false).value).to eq(timezone_name)
+ end
+ end
end
end
diff --git a/spec/features/projects/settings/repository_settings_spec.rb b/spec/features/projects/settings/repository_settings_spec.rb
index f7de769cee9..8c7bc192c50 100644
--- a/spec/features/projects/settings/repository_settings_spec.rb
+++ b/spec/features/projects/settings/repository_settings_spec.rb
@@ -236,5 +236,17 @@ describe 'Projects > Settings > Repository settings' do
expect(mirrored_project.remote_mirrors.count).to eq(0)
end
end
+
+ it 'shows a disabled mirror' do
+ create(:remote_mirror, project: project, enabled: false)
+
+ visit project_settings_repository_path(project)
+
+ mirror = find('.qa-mirrored-repository-row')
+
+ expect(mirror).to have_selector('.qa-delete-mirror')
+ expect(mirror).to have_selector('.qa-disabled-mirror-badge')
+ expect(mirror).not_to have_selector('.qa-update-now-button')
+ end
end
end
diff --git a/spec/finders/issues_finder_spec.rb b/spec/finders/issues_finder_spec.rb
index 6a47cd013f8..89fdaceaa9f 100644
--- a/spec/finders/issues_finder_spec.rb
+++ b/spec/finders/issues_finder_spec.rb
@@ -641,9 +641,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
@@ -660,9 +658,7 @@ describe IssuesFinder do
end
it 'filters by confidentiality' do
- expect(Issue).to receive(:where).with(a_string_matching('confidential'), anything)
-
- subject
+ expect(subject.to_sql).to match("issues.confidential")
end
end
diff --git a/spec/finders/merge_requests_finder_spec.rb b/spec/finders/merge_requests_finder_spec.rb
index 117f4a03735..da5e9dab058 100644
--- a/spec/finders/merge_requests_finder_spec.rb
+++ b/spec/finders/merge_requests_finder_spec.rb
@@ -31,7 +31,7 @@ describe MergeRequestsFinder do
end
context 'filtering by group' do
- it 'includes all merge requests when user has access exceluding merge requests from projects the user does not have access to' do
+ it 'includes all merge requests when user has access excluding merge requests from projects the user does not have access to' do
private_project = allow_gitaly_n_plus_1 { create(:project, :private, group: group) }
private_project.add_guest(user)
create(:merge_request, :simple, author: user, source_project: private_project, target_project: private_project)
diff --git a/spec/frontend/notes/stores/utils_spec.js b/spec/frontend/notes/stores/utils_spec.js
new file mode 100644
index 00000000000..b31b7491334
--- /dev/null
+++ b/spec/frontend/notes/stores/utils_spec.js
@@ -0,0 +1,17 @@
+import { hasQuickActions } from '~/notes/stores/utils';
+
+describe('hasQuickActions', () => {
+ it.each`
+ input | expected
+ ${'some comment'} | ${false}
+ ${'/quickaction'} | ${true}
+ ${'some comment with\n/quickaction'} | ${true}
+ `('returns $expected for $input', ({ input, expected }) => {
+ expect(hasQuickActions(input)).toBe(expected);
+ });
+
+ it('is stateless', () => {
+ expect(hasQuickActions('some comment')).toBe(hasQuickActions('some comment'));
+ expect(hasQuickActions('/quickaction')).toBe(hasQuickActions('/quickaction'));
+ });
+});
diff --git a/spec/frontend/operation_settings/components/external_dashboard_spec.js b/spec/frontend/operation_settings/components/external_dashboard_spec.js
new file mode 100644
index 00000000000..de1dd219fe0
--- /dev/null
+++ b/spec/frontend/operation_settings/components/external_dashboard_spec.js
@@ -0,0 +1,100 @@
+import { shallowMount } from '@vue/test-utils';
+import { GlButton, GlLink, GlFormGroup, GlFormInput } from '@gitlab/ui';
+import ExternalDashboard from '~/operation_settings/components/external_dashboard.vue';
+import { TEST_HOST } from 'helpers/test_constants';
+
+describe('operation settings external dashboard component', () => {
+ let wrapper;
+ const externalDashboardPath = `http://mock-external-domain.com/external/dashboard/path`;
+ const externalDashboardHelpPagePath = `${TEST_HOST}/help/page/path`;
+
+ beforeEach(() => {
+ wrapper = shallowMount(ExternalDashboard, {
+ propsData: {
+ externalDashboardPath,
+ externalDashboardHelpPagePath,
+ },
+ });
+ });
+
+ it('renders header text', () => {
+ expect(wrapper.find('.js-section-header').text()).toBe('External Dashboard');
+ });
+
+ describe('sub-header', () => {
+ let subHeader;
+
+ beforeEach(() => {
+ subHeader = wrapper.find('.js-section-sub-header');
+ });
+
+ it('renders descriptive text', () => {
+ expect(subHeader.text()).toContain(
+ 'Add a button to the metrics dashboard linking directly to your existing external dashboards.',
+ );
+ });
+
+ it('renders help page link', () => {
+ const link = subHeader.find(GlLink);
+
+ expect(link.text()).toBe('Learn more');
+ expect(link.attributes().href).toBe(externalDashboardHelpPagePath);
+ });
+ });
+
+ describe('form', () => {
+ let form;
+
+ beforeEach(() => {
+ form = wrapper.find('form');
+ });
+
+ describe('external dashboard url', () => {
+ describe('input label', () => {
+ let formGroup;
+
+ beforeEach(() => {
+ formGroup = form.find(GlFormGroup);
+ });
+
+ it('uses label text', () => {
+ expect(formGroup.attributes().label).toBe('Full dashboard URL');
+ });
+
+ it('uses description text', () => {
+ expect(formGroup.attributes().description).toBe(
+ 'Enter the URL of the dashboard you want to link to',
+ );
+ });
+ });
+
+ describe('input field', () => {
+ let input;
+
+ beforeEach(() => {
+ input = form.find(GlFormInput);
+ });
+
+ it('defaults to externalDashboardPath prop', () => {
+ expect(input.attributes().value).toBe(externalDashboardPath);
+ });
+
+ it('uses a placeholder', () => {
+ expect(input.attributes().placeholder).toBe('https://my-org.gitlab.io/my-dashboards');
+ });
+ });
+
+ describe('submit button', () => {
+ let submit;
+
+ beforeEach(() => {
+ submit = form.find(GlButton);
+ });
+
+ it('renders button label', () => {
+ expect(submit.text()).toBe('Save Changes');
+ });
+ });
+ });
+ });
+});
diff --git a/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
new file mode 100644
index 00000000000..3b6f67457ad
--- /dev/null
+++ b/spec/frontend/vue_shared/components/markdown/suggestion_diff_header_spec.js
@@ -0,0 +1,103 @@
+import { GlLoadingIcon } from '@gitlab/ui';
+import { shallowMount, createLocalVue } from '@vue/test-utils';
+import SuggestionDiffHeader from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
+
+const localVue = createLocalVue();
+
+const DEFAULT_PROPS = {
+ canApply: true,
+ isApplied: false,
+ helpPagePath: 'path_to_docs',
+};
+
+describe('Suggestion Diff component', () => {
+ let wrapper;
+
+ const createComponent = props => {
+ wrapper = shallowMount(localVue.extend(SuggestionDiffHeader), {
+ propsData: {
+ ...DEFAULT_PROPS,
+ ...props,
+ },
+ localVue,
+ sync: false,
+ });
+ };
+
+ afterEach(() => {
+ wrapper.destroy();
+ });
+
+ const findApplyButton = () => wrapper.find('.qa-apply-btn');
+ const findHeader = () => wrapper.find('.qa-suggestion-diff-header');
+ const findHelpButton = () => wrapper.find('.js-help-btn');
+ const findLoading = () => wrapper.find(GlLoadingIcon);
+
+ it('renders a suggestion header', () => {
+ createComponent();
+
+ const header = findHeader();
+
+ expect(header.exists()).toBe(true);
+ expect(header.html().includes('Suggested change')).toBe(true);
+ });
+
+ it('renders a help button', () => {
+ createComponent();
+
+ expect(findHelpButton().exists()).toBe(true);
+ });
+
+ it('renders an apply button', () => {
+ createComponent();
+
+ const applyBtn = findApplyButton();
+
+ expect(applyBtn.exists()).toBe(true);
+ expect(applyBtn.html().includes('Apply suggestion')).toBe(true);
+ });
+
+ it('does not render an apply button if `canApply` is set to false', () => {
+ createComponent({ canApply: false });
+
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ describe('when apply suggestion is clicked', () => {
+ beforeEach(done => {
+ createComponent();
+
+ findApplyButton().vm.$emit('click');
+
+ wrapper.vm.$nextTick(done);
+ });
+
+ it('emits apply', () => {
+ expect(wrapper.emittedByOrder()).toEqual([{ name: 'apply', args: [expect.any(Function)] }]);
+ });
+
+ it('hides apply button', () => {
+ expect(findApplyButton().exists()).toBe(false);
+ });
+
+ it('shows loading', () => {
+ expect(findLoading().exists()).toBe(true);
+ expect(wrapper.text()).toContain('Applying suggestion');
+ });
+
+ it('when callback of apply is called, hides loading', done => {
+ const [callback] = wrapper.emitted().apply[0];
+
+ callback();
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(findApplyButton().exists()).toBe(true);
+ expect(findLoading().exists()).toBe(false);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/monitoring/dashboard_spec.js b/spec/javascripts/monitoring/dashboard_spec.js
index 5c28840d3a4..e9bd6050d68 100644
--- a/spec/javascripts/monitoring/dashboard_spec.js
+++ b/spec/javascripts/monitoring/dashboard_spec.js
@@ -20,6 +20,9 @@ const propsData = {
emptyUnableToConnectSvgPath: '/path/to/unable-to-connect.svg',
environmentsEndpoint: '/root/hello-prometheus/environments/35',
currentEnvironmentName: 'production',
+ customMetricsAvailable: false,
+ customMetricsPath: '',
+ validateQueryPath: '',
};
export default propsData;
@@ -37,6 +40,9 @@ describe('Dashboard', () => {
window.gon = {
...window.gon,
ee: false,
+ features: {
+ grafanaDashboardLink: true,
+ },
};
mock = new MockAdapter(axios);
@@ -160,7 +166,7 @@ describe('Dashboard', () => {
});
});
- it('renders the environments dropdown with a single is-active element', done => {
+ it('renders the environments dropdown with a single active element', done => {
const component = new DashboardComponent({
el: document.querySelector('.prometheus-graphs'),
propsData: {
@@ -175,7 +181,7 @@ describe('Dashboard', () => {
setTimeout(() => {
const dropdownItems = component.$el.querySelectorAll(
- '.js-environments-dropdown .dropdown-item.is-active',
+ '.js-environments-dropdown .dropdown-item[active="true"]',
);
expect(dropdownItems.length).toEqual(1);
@@ -323,4 +329,63 @@ describe('Dashboard', () => {
.catch(done.fail);
});
});
+
+ describe('external dashboard link', () => {
+ let component;
+
+ beforeEach(() => {
+ mock.onGet(mockApiEndpoint).reply(200, metricsGroupsAPIResponse);
+ });
+
+ afterEach(() => {
+ component.$destroy();
+ });
+
+ describe('with feature flag enabled', () => {
+ beforeEach(() => {
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardPath: '/mockPath',
+ },
+ });
+ });
+
+ it('shows the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link').innerText).toContain(
+ 'View full dashboard',
+ );
+ done();
+ });
+ });
+ });
+
+ describe('without feature flage enabled', () => {
+ beforeEach(() => {
+ window.gon.features.grafanaDashboardLink = false;
+ component = new DashboardComponent({
+ el: document.querySelector('.prometheus-graphs'),
+ propsData: {
+ ...propsData,
+ hasMetrics: true,
+ showPanels: false,
+ showTimeWindowDropdown: false,
+ externalDashboardPath: '',
+ },
+ });
+ });
+
+ it('does not show the link', done => {
+ setTimeout(() => {
+ expect(component.$el.querySelector('.js-external-dashboard-link')).toBe(null);
+ done();
+ });
+ });
+ });
+ });
});
diff --git a/spec/javascripts/notes/stores/actions_spec.js b/spec/javascripts/notes/stores/actions_spec.js
index 94ce6d8e222..7a9f32ddcff 100644
--- a/spec/javascripts/notes/stores/actions_spec.js
+++ b/spec/javascripts/notes/stores/actions_spec.js
@@ -3,11 +3,12 @@ import $ from 'jquery';
import _ from 'underscore';
import { TEST_HOST } from 'spec/test_constants';
import { headersInterceptor } from 'spec/helpers/vue_resource_helper';
-import * as actions from '~/notes/stores/actions';
+import actionsModule, * as actions from '~/notes/stores/actions';
import * as mutationTypes from '~/notes/stores/mutation_types';
import * as notesConstants from '~/notes/constants';
import createStore from '~/notes/stores';
import mrWidgetEventHub from '~/vue_merge_request_widget/event_hub';
+import service from '~/notes/services/notes_service';
import testAction from '../../helpers/vuex_action_helper';
import { resetStore } from '../helpers';
import {
@@ -18,11 +19,21 @@ import {
individualNote,
} from '../mock_data';
+const TEST_ERROR_MESSAGE = 'Test error message';
+
describe('Actions Notes Store', () => {
+ let commit;
+ let dispatch;
+ let state;
let store;
+ let flashSpy;
beforeEach(() => {
store = createStore();
+ commit = jasmine.createSpy('commit');
+ dispatch = jasmine.createSpy('dispatch');
+ state = {};
+ flashSpy = spyOnDependency(actionsModule, 'Flash');
});
afterEach(() => {
@@ -604,21 +615,6 @@ describe('Actions Notes Store', () => {
});
describe('updateOrCreateNotes', () => {
- let commit;
- let dispatch;
- let state;
-
- beforeEach(() => {
- commit = jasmine.createSpy('commit');
- dispatch = jasmine.createSpy('dispatch');
- state = {};
- });
-
- afterEach(() => {
- commit.calls.reset();
- dispatch.calls.reset();
- });
-
it('Updates existing note', () => {
const note = { id: 1234 };
const getters = { notesById: { 1234: note } };
@@ -751,4 +747,151 @@ describe('Actions Notes Store', () => {
);
});
});
+
+ describe('resolveDiscussion', () => {
+ let getters;
+ let discussionId;
+
+ beforeEach(() => {
+ discussionId = discussionMock.id;
+ state.discussions = [discussionMock];
+ getters = {
+ isDiscussionResolved: () => false,
+ };
+ });
+
+ it('when unresolved, dispatches action', done => {
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [
+ {
+ type: 'toggleResolveNote',
+ payload: {
+ endpoint: discussionMock.resolve_path,
+ isResolved: false,
+ discussion: true,
+ },
+ },
+ ],
+ done,
+ );
+ });
+
+ it('when resolved, does nothing', done => {
+ getters.isDiscussionResolved = id => id === discussionId;
+
+ testAction(
+ actions.resolveDiscussion,
+ { discussionId },
+ { ...state, ...getters },
+ [],
+ [],
+ done,
+ );
+ });
+ });
+
+ describe('saveNote', () => {
+ const payload = { endpoint: TEST_HOST, data: { 'note[note]': 'some text' } };
+
+ describe('if response contains errors', () => {
+ const res = { errors: { something: ['went wrong'] } };
+
+ it('throws an error', done => {
+ actions
+ .saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.resolve(res),
+ },
+ payload,
+ )
+ .then(() => done.fail('Expected error to be thrown!'))
+ .catch(error => {
+ expect(error.message).toBe('Failed to save comment!');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+
+ describe('if response contains no errors', () => {
+ const res = { valid: true };
+
+ it('returns the response', done => {
+ actions
+ .saveNote(
+ {
+ commit() {},
+ dispatch: () => Promise.resolve(res),
+ },
+ payload,
+ )
+ .then(data => {
+ expect(data).toBe(res);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('submitSuggestion', () => {
+ const discussionId = 'discussion-id';
+ const noteId = 'note-id';
+ const suggestionId = 'suggestion-id';
+ let flashContainer;
+
+ beforeEach(() => {
+ spyOn(service, 'applySuggestion');
+ dispatch.and.returnValue(Promise.resolve());
+ service.applySuggestion.and.returnValue(Promise.resolve());
+ flashContainer = {};
+ });
+
+ const testSubmitSuggestion = (done, expectFn) => {
+ actions
+ .submitSuggestion(
+ { commit, dispatch },
+ { discussionId, noteId, suggestionId, flashContainer },
+ )
+ .then(expectFn)
+ .then(done)
+ .catch(done.fail);
+ };
+
+ it('when service success, commits and resolves discussion', done => {
+ testSubmitSuggestion(done, () => {
+ expect(commit.calls.allArgs()).toEqual([
+ [mutationTypes.APPLY_SUGGESTION, { discussionId, noteId, suggestionId }],
+ ]);
+
+ expect(dispatch.calls.allArgs()).toEqual([['resolveDiscussion', { discussionId }]]);
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+
+ it('when service fails, flashes error message', done => {
+ const response = { response: { data: { message: TEST_ERROR_MESSAGE } } };
+
+ service.applySuggestion.and.returnValue(Promise.reject(response));
+
+ testSubmitSuggestion(done, () => {
+ expect(commit).not.toHaveBeenCalled();
+ expect(dispatch).not.toHaveBeenCalled();
+ expect(flashSpy).toHaveBeenCalledWith(`${TEST_ERROR_MESSAGE}.`, 'alert', flashContainer);
+ });
+ });
+
+ it('when resolve discussion fails, fail gracefully', done => {
+ dispatch.and.returnValue(Promise.reject());
+
+ testSubmitSuggestion(done, () => {
+ expect(flashSpy).not.toHaveBeenCalled();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
index a89952ee435..5f4dba5ecb9 100644
--- a/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
+++ b/spec/javascripts/pages/projects/pipeline_schedules/shared/components/timezone_dropdown_spec.js
@@ -3,6 +3,7 @@ import GLDropdown from '~/gl_dropdown'; // eslint-disable-line no-unused-vars
import TimezoneDropdown, {
formatUtcOffset,
formatTimezone,
+ findTimezoneByIdentifier,
} from '~/pages/projects/pipeline_schedules/shared/components/timezone_dropdown';
describe('Timezone Dropdown', function() {
@@ -12,6 +13,7 @@ describe('Timezone Dropdown', function() {
let $dropdownEl = null;
let $wrapper = null;
const tzListSel = '.dropdown-content ul li a.is-active';
+ const tzDropdownToggleText = '.dropdown-toggle-text';
describe('Initialize', () => {
describe('with dropdown already loaded', () => {
@@ -94,6 +96,36 @@ describe('Timezone Dropdown', function() {
expect(onSelectTimezone).toHaveBeenCalled();
});
+
+ it('will correctly set the dropdown label if a timezone identifier is set on the inputEl', () => {
+ $inputEl.val('America/St_Johns');
+
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ displayFormat: selectedItem => formatTimezone(selectedItem),
+ });
+
+ expect($wrapper.find(tzDropdownToggleText).html()).toEqual('[UTC - 2.5] Newfoundland');
+ });
+
+ it('will call a provided `displayFormat` handler to format the dropdown value', () => {
+ const displayFormat = jasmine.createSpy('displayFormat');
+ // eslint-disable-next-line no-new
+ new TimezoneDropdown({
+ $inputEl,
+ $dropdownEl,
+ displayFormat,
+ });
+
+ $wrapper
+ .find(tzListSel)
+ .first()
+ .trigger('click');
+
+ expect(displayFormat).toHaveBeenCalled();
+ });
});
});
@@ -164,4 +196,49 @@ describe('Timezone Dropdown', function() {
).toEqual('[UTC 0] Accra');
});
});
+
+ describe('findTimezoneByIdentifier', () => {
+ const tzList = [
+ {
+ identifier: 'Asia/Tokyo',
+ name: 'Sapporo',
+ offset: 32400,
+ },
+ {
+ identifier: 'Asia/Hong_Kong',
+ name: 'Hong Kong',
+ offset: 28800,
+ },
+ {
+ identifier: 'Asia/Dhaka',
+ name: 'Dhaka',
+ offset: 21600,
+ },
+ ];
+
+ const identifier = 'Asia/Dhaka';
+ it('returns the correct object if the identifier exists', () => {
+ const res = findTimezoneByIdentifier(tzList, identifier);
+
+ expect(res).toBeTruthy();
+ expect(res).toBe(tzList[2]);
+ });
+
+ it('returns null if it doesnt find the identifier', () => {
+ const res = findTimezoneByIdentifier(tzList, 'Australia/Melbourne');
+
+ expect(res).toBeNull();
+ });
+
+ it('returns null if there is no identifier given', () => {
+ expect(findTimezoneByIdentifier(tzList)).toBeNull();
+ expect(findTimezoneByIdentifier(tzList, '')).toBeNull();
+ });
+
+ it('returns null if there is an empty or invalid array given', () => {
+ expect(findTimezoneByIdentifier([], identifier)).toBeNull();
+ expect(findTimezoneByIdentifier(null, identifier)).toBeNull();
+ expect(findTimezoneByIdentifier(undefined, identifier)).toBeNull();
+ });
+ });
});
diff --git a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
index de213210cfc..8ac6e6a7b44 100644
--- a/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
+++ b/spec/javascripts/vue_mr_widget/components/mr_widget_pipeline_spec.js
@@ -78,6 +78,19 @@ describe('MRWidgetPipeline', () => {
);
});
+ it('should render CI error when no pipeline is provided', () => {
+ vm = mountComponent(Component, {
+ pipeline: {},
+ hasCi: true,
+ ciStatus: 'success',
+ troubleshootingDocsPath: 'help',
+ });
+
+ expect(vm.$el.querySelector('.media-body').textContent.trim()).toContain(
+ 'Could not retrieve the pipeline status. For troubleshooting steps, read the documentation.',
+ );
+ });
+
describe('with a pipeline', () => {
beforeEach(() => {
vm = mountComponent(Component, {
diff --git a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js b/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
deleted file mode 100644
index 12ee804f668..00000000000
--- a/spec/javascripts/vue_shared/components/markdown/suggestion_diff_header_spec.js
+++ /dev/null
@@ -1,75 +0,0 @@
-import Vue from 'vue';
-import SuggestionDiffHeaderComponent from '~/vue_shared/components/markdown/suggestion_diff_header.vue';
-
-const MOCK_DATA = {
- canApply: true,
- isApplied: false,
- helpPagePath: 'path_to_docs',
-};
-
-describe('Suggestion Diff component', () => {
- let vm;
-
- function createComponent(propsData) {
- const Component = Vue.extend(SuggestionDiffHeaderComponent);
-
- return new Component({
- propsData,
- }).$mount();
- }
-
- beforeEach(done => {
- vm = createComponent(MOCK_DATA);
- Vue.nextTick(done);
- });
-
- describe('init', () => {
- it('renders a suggestion header', () => {
- const header = vm.$el.querySelector('.qa-suggestion-diff-header');
-
- expect(header).not.toBeNull();
- expect(header.innerHTML.includes('Suggested change')).toBe(true);
- });
-
- it('renders a help button', () => {
- const helpBtn = vm.$el.querySelector('.js-help-btn');
-
- expect(helpBtn).not.toBeNull();
- });
-
- it('renders an apply button', () => {
- const applyBtn = vm.$el.querySelector('.qa-apply-btn');
-
- expect(applyBtn).not.toBeNull();
- expect(applyBtn.innerHTML.includes('Apply suggestion')).toBe(true);
- });
-
- it('does not render an apply button if `canApply` is set to false', () => {
- const props = Object.assign(MOCK_DATA, { canApply: false });
-
- vm = createComponent(props);
-
- expect(vm.$el.querySelector('.qa-apply-btn')).toBeNull();
- });
- });
-
- describe('applySuggestion', () => {
- it('emits when the apply button is clicked', () => {
- const props = Object.assign(MOCK_DATA, { canApply: true });
-
- vm = createComponent(props);
- spyOn(vm, '$emit');
- vm.applySuggestion();
-
- expect(vm.$emit).toHaveBeenCalled();
- });
-
- it('does not emit when the canApply is set to false', () => {
- spyOn(vm, '$emit');
- vm.canApply = false;
- vm.applySuggestion();
-
- expect(vm.$emit).not.toHaveBeenCalled();
- });
- });
-});
diff --git a/spec/lib/api/helpers/related_resources_helpers_spec.rb b/spec/lib/api/helpers/related_resources_helpers_spec.rb
index 66af7f81535..99fe8795d91 100644
--- a/spec/lib/api/helpers/related_resources_helpers_spec.rb
+++ b/spec/lib/api/helpers/related_resources_helpers_spec.rb
@@ -5,6 +5,40 @@ describe API::Helpers::RelatedResourcesHelpers do
Class.new.include(described_class).new
end
+ describe '#expose_path' do
+ let(:path) { '/api/v4/awesome_endpoint' }
+
+ context 'empty relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'slash relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq(path)
+ end
+ end
+
+ context 'with relative URL root' do
+ before do
+ stub_config_setting(relative_url_root: '/gitlab/root')
+ end
+
+ it 'returns the existing path' do
+ expect(helpers.expose_path(path)).to eq("/gitlab/root" + path)
+ end
+ end
+ end
+
describe '#expose_url' do
let(:path) { '/api/v4/awesome_endpoint' }
subject(:url) { helpers.expose_url(path) }
diff --git a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
index 7972ff253fe..aaf8c9fa2a0 100644
--- a/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/ruby_sampler_spec.rb
@@ -10,17 +10,20 @@ describe Gitlab::Metrics::Samplers::RubySampler do
describe '#sample' do
it 'samples various statistics' do
- expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:cpu_time)
expect(Gitlab::Metrics::System).to receive(:file_descriptor_count)
+ expect(Gitlab::Metrics::System).to receive(:memory_usage)
+ expect(Gitlab::Metrics::System).to receive(:process_start_time)
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors)
expect(sampler).to receive(:sample_gc)
sampler.sample
end
- it 'adds a metric containing the memory usage' do
+ it 'adds a metric containing the process resident memory bytes' do
expect(Gitlab::Metrics::System).to receive(:memory_usage).and_return(9000)
- expect(sampler.metrics[:memory_usage]).to receive(:set).with({}, 9000)
+ expect(sampler.metrics[:process_resident_memory_bytes]).to receive(:set).with({}, 9000)
sampler.sample
end
@@ -34,6 +37,27 @@ describe Gitlab::Metrics::Samplers::RubySampler do
sampler.sample
end
+ it 'adds a metric containing the process total cpu time' do
+ expect(Gitlab::Metrics::System).to receive(:cpu_time).and_return(0.51)
+ expect(sampler.metrics[:process_cpu_seconds_total]).to receive(:set).with({}, 0.51)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process start time' do
+ expect(Gitlab::Metrics::System).to receive(:process_start_time).and_return(12345)
+ expect(sampler.metrics[:process_start_time_seconds]).to receive(:set).with({}, 12345)
+
+ sampler.sample
+ end
+
+ it 'adds a metric containing the process max file descriptors' do
+ expect(Gitlab::Metrics::System).to receive(:max_open_file_descriptors).and_return(1024)
+ expect(sampler.metrics[:process_max_fds]).to receive(:set).with({}, 1024)
+
+ sampler.sample
+ end
+
it 'clears any GC profiles' do
expect(GC::Profiler).to receive(:clear)
diff --git a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
index 4b03f3c2532..090e456644f 100644
--- a/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
+++ b/spec/lib/gitlab/metrics/samplers/unicorn_sampler_spec.rb
@@ -39,8 +39,8 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'unix', socket_address: socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
@@ -50,7 +50,6 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
context 'unicorn listens on tcp sockets' do
let(:tcp_socket_address) { '0.0.0.0:8080' }
let(:tcp_sockets) { [tcp_socket_address] }
-
before do
allow(unicorn).to receive(:listener_names).and_return(tcp_sockets)
end
@@ -71,13 +70,29 @@ describe Gitlab::Metrics::Samplers::UnicornSampler do
it 'updates metrics type unix and with addr' do
labels = { socket_type: 'tcp', socket_address: tcp_socket_address }
- expect(subject).to receive_message_chain(:unicorn_active_connections, :set).with(labels, 'active')
- expect(subject).to receive_message_chain(:unicorn_queued_connections, :set).with(labels, 'queued')
+ expect(subject.metrics[:unicorn_active_connections]).to receive(:set).with(labels, 'active')
+ expect(subject.metrics[:unicorn_queued_connections]).to receive(:set).with(labels, 'queued')
subject.sample
end
end
end
+
+ context 'additional metrics' do
+ let(:unicorn_workers) { 2 }
+
+ before do
+ allow(unicorn).to receive(:listener_names).and_return([""])
+ allow(::Gitlab::Metrics::System).to receive(:cpu_time).and_return(3.14)
+ allow(subject).to receive(:unicorn_workers_count).and_return(unicorn_workers)
+ end
+
+ it "sets additional metrics" do
+ expect(subject.metrics[:unicorn_workers]).to receive(:set).with({}, unicorn_workers)
+
+ subject.sample
+ end
+ end
end
describe '#start' do
diff --git a/spec/lib/gitlab/metrics/system_spec.rb b/spec/lib/gitlab/metrics/system_spec.rb
index 14afcdf5daa..b0603d96eb2 100644
--- a/spec/lib/gitlab/metrics/system_spec.rb
+++ b/spec/lib/gitlab/metrics/system_spec.rb
@@ -13,6 +13,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to be > 0
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns the max allowed open file descriptors' do
+ expect(described_class.max_open_file_descriptors).to be > 0
+ end
+ end
+
+ describe '.process_start_time' do
+ it 'returns the process start time' do
+ expect(described_class.process_start_time).to be > 0
+ end
+ end
else
describe '.memory_usage' do
it 'returns 0.0' do
@@ -25,6 +37,18 @@ describe Gitlab::Metrics::System do
expect(described_class.file_descriptor_count).to eq(0)
end
end
+
+ describe '.max_open_file_descriptors' do
+ it 'returns 0' do
+ expect(described_class.max_open_file_descriptors).to eq(0)
+ end
+ end
+
+ describe 'process_start_time' do
+ it 'returns 0' do
+ expect(described_class.process_start_time).to eq(0)
+ end
+ end
end
describe '.cpu_time' do
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index bb0257e7456..2a17bd6002e 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3164,61 +3164,105 @@ describe Project do
end
describe '.with_feature_available_for_user' do
- let!(:user) { create(:user) }
- let!(:feature) { MergeRequest }
- let!(:project) { create(:project, :public, :merge_requests_enabled) }
+ let(:user) { create(:user) }
+ let(:feature) { MergeRequest }
subject { described_class.with_feature_available_for_user(feature, user) }
- context 'when user has access to project' do
- subject { described_class.with_feature_available_for_user(feature, user) }
+ shared_examples 'feature disabled' do
+ let(:project) { create(:project, :public, :merge_requests_disabled) }
+
+ it 'does not return projects with the project feature disabled' do
+ is_expected.not_to include(project)
+ end
+ end
+
+ shared_examples 'feature public' do
+ let(:project) { create(:project, :public, :merge_requests_public) }
+
+ it 'returns projects with the project feature public' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature enabled' do
+ let(:project) { create(:project, :public, :merge_requests_enabled) }
+
+ it 'returns projects with the project feature enabled' do
+ is_expected.to include(project)
+ end
+ end
+
+ shared_examples 'feature access level is nil' do
+ let(:project) { create(:project, :public) }
+
+ it 'returns projects with the project feature access level nil' do
+ project.project_feature.update(merge_requests_access_level: nil)
+
+ is_expected.to include(project)
+ end
+ end
+ context 'with user' do
before do
project.add_guest(user)
end
- context 'when public project' do
- context 'when feature is public' do
- it 'returns project' do
- is_expected.to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ context 'when user does not has access to the feature' do
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
end
end
- context 'when feature is private' do
- let!(:project) { create(:project, :public, :merge_requests_private) }
-
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ context 'when user has access to the feature' do
+ it 'returns projects with the project feature private' do
+ project.add_reporter(user)
is_expected.to include(project)
end
-
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
- end
end
end
+ end
- context 'when private project' do
- let!(:project) { create(:project) }
+ context 'user is an admin' do
+ let(:user) { create(:user, :admin) }
- it 'returns project when user has access to the feature' do
- project.add_maintainer(user)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
- is_expected.to include(project)
- end
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
- it 'does not return project when user does not have the minimum access level required' do
- is_expected.not_to include(project)
+ it 'returns projects with the project feature private' do
+ is_expected.to include(project)
end
end
end
- context 'when user does not have access to project' do
- let!(:project) { create(:project) }
+ context 'without user' do
+ let(:user) { nil }
- it 'does not return project when user cant access project' do
- is_expected.not_to include(project)
+ it_behaves_like 'feature disabled'
+ it_behaves_like 'feature public'
+ it_behaves_like 'feature enabled'
+ it_behaves_like 'feature access level is nil'
+
+ context 'when feature is private' do
+ let(:project) { create(:project, :public, :merge_requests_private) }
+
+ it 'does not return projects with the project feature private' do
+ is_expected.not_to include(project)
+ end
end
end
end
diff --git a/spec/models/remote_mirror_spec.rb b/spec/models/remote_mirror_spec.rb
index f743dfed31f..e14b19db915 100644
--- a/spec/models/remote_mirror_spec.rb
+++ b/spec/models/remote_mirror_spec.rb
@@ -373,6 +373,22 @@ describe RemoteMirror, :mailer do
end
end
+ describe '#disabled?' do
+ subject { remote_mirror.disabled? }
+
+ context 'when disabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: false) }
+
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when enabled' do
+ let(:remote_mirror) { build(:remote_mirror, enabled: true) }
+
+ it { is_expected.to be_falsy }
+ end
+ end
+
def create_mirror(params)
project = FactoryBot.create(:project, :repository)
project.remote_mirrors.create!(params)
diff --git a/spec/models/user_preference_spec.rb b/spec/models/user_preference_spec.rb
index b2ef17a81d4..e09c91e874a 100644
--- a/spec/models/user_preference_spec.rb
+++ b/spec/models/user_preference_spec.rb
@@ -73,4 +73,10 @@ describe UserPreference do
it_behaves_like 'a sort_by preference'
end
end
+
+ describe '#timezone' do
+ it 'returns server time as default' do
+ expect(user_preference.timezone).to eq(Time.zone.tzinfo.name)
+ end
+ end
end
diff --git a/spec/requests/api/merge_requests_spec.rb b/spec/requests/api/merge_requests_spec.rb
index 5c94a87529b..007f3517e64 100644
--- a/spec/requests/api/merge_requests_spec.rb
+++ b/spec/requests/api/merge_requests_spec.rb
@@ -1495,6 +1495,33 @@ describe API::MergeRequests do
expect(json_response['merge_when_pipeline_succeeds']).to eq(true)
end
+ context 'when the MR requires pipeline success' do
+ it 'returns 405 if the pipeline is missing' do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:merge_when_pipeline_succeeds).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user)
+
+ expect(response).to have_gitlab_http_status(405)
+ expect(json_response['message']).to eq('Not allowed: pipeline does not exist')
+ end
+ end
+
+ context 'when the request requires pipeline success' do
+ it 'returns 405 if the pipeline is missing' do
+ allow_any_instance_of(MergeRequest)
+ .to receive(:merge_when_pipeline_succeeds).and_return(true)
+ allow_any_instance_of(MergeRequest).to receive(:head_pipeline).and_return(nil)
+
+ put api("/projects/#{project.id}/merge_requests/#{merge_request.iid}/merge", user),
+ params: { merge_when_pipeline_succeeds: true }
+
+ expect(response).to have_gitlab_http_status(405)
+ expect(json_response['message']).to eq('Not allowed: pipeline does not exist')
+ end
+ end
+
it "returns 404 for an invalid merge request IID" do
put api("/projects/#{project.id}/merge_requests/12345/merge", user)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 577f61ae8d0..16d306f39cd 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -504,8 +504,9 @@ describe API::Projects do
project4.add_reporter(user2)
end
- it 'returns an array of groups the user has at least developer access' do
+ it 'returns an array of projects the user has at least developer access' do
get api('/projects', user2), params: { min_access_level: 30 }
+
expect(response).to have_gitlab_http_status(200)
expect(response).to include_pagination_headers
expect(json_response).to be_an Array
diff --git a/spec/services/users/update_service_spec.rb b/spec/services/users/update_service_spec.rb
index e04c05418b0..9384287f98a 100644
--- a/spec/services/users/update_service_spec.rb
+++ b/spec/services/users/update_service_spec.rb
@@ -13,6 +13,15 @@ describe Users::UpdateService do
expect(user.name).to eq('New Name')
end
+ it 'updates time preferences' do
+ result = update_user(user, timezone: 'Europe/Warsaw', time_display_relative: true, time_format_in_24h: false)
+
+ expect(result).to eq(status: :success)
+ expect(user.reload.timezone).to eq('Europe/Warsaw')
+ expect(user.time_display_relative).to eq(true)
+ expect(user.time_format_in_24h).to eq(false)
+ end
+
it 'returns an error result when record cannot be updated' do
result = {}
expect do
diff --git a/spec/support/capybara.rb b/spec/support/capybara.rb
index 18a7a392c12..875a9a76e12 100644
--- a/spec/support/capybara.rb
+++ b/spec/support/capybara.rb
@@ -17,6 +17,8 @@ JS_CONSOLE_FILTER = Regexp.union([
"Download the Vue Devtools extension"
])
+CAPYBARA_WINDOW_SIZE = [1366, 768].freeze
+
Capybara.register_driver :chrome do |app|
capabilities = Selenium::WebDriver::Remote::Capabilities.chrome(
# This enables access to logs with `page.driver.manage.get_log(:browser)`
@@ -29,7 +31,7 @@ Capybara.register_driver :chrome do |app|
)
options = Selenium::WebDriver::Chrome::Options.new
- options.add_argument("window-size=1240,1400")
+ options.add_argument("window-size=#{CAPYBARA_WINDOW_SIZE.join(',')}")
# Chrome won't work properly in a Docker container in sandbox mode
options.add_argument("no-sandbox")
@@ -78,8 +80,11 @@ RSpec.configure do |config|
protocol: 'http')
# reset window size between tests
- unless session.current_window.size == [1240, 1400]
- session.current_window.resize_to(1240, 1400) rescue nil
+ unless session.current_window.size == CAPYBARA_WINDOW_SIZE
+ begin
+ session.current_window.resize_to(*CAPYBARA_WINDOW_SIZE)
+ rescue # ?
+ end
end
end
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 89517fde6e2..38f30a14409 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -23,8 +23,18 @@ module Spec
def preview_note(text)
page.within('.js-main-target-form') do
- fill_in('note[note]', with: text)
+ filled_text = fill_in('note[note]', with: text)
+
+ begin
+ # Dismiss quick action prompt if it appears
+ filled_text.parent.send_keys(:escape)
+ rescue Selenium::WebDriver::Error::ElementNotInteractableError
+ # It's fine if we can't escape when there's no prompt.
+ end
+
click_on('Preview')
+
+ yield if block_given?
end
end
end
diff --git a/spec/support/helpers/mobile_helpers.rb b/spec/support/helpers/mobile_helpers.rb
index 9dc1f1de436..4230d315d9b 100644
--- a/spec/support/helpers/mobile_helpers.rb
+++ b/spec/support/helpers/mobile_helpers.rb
@@ -8,7 +8,7 @@ module MobileHelpers
end
def restore_window_size
- resize_window(1366, 768)
+ resize_window(*CAPYBARA_WINDOW_SIZE)
end
def resize_window(width, height)
diff --git a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
index e0d0b790a0e..a79a61bc708 100644
--- a/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
+++ b/spec/support/shared_examples/quick_actions/issuable/close_quick_action_shared_examples.rb
@@ -1,6 +1,8 @@
# frozen_string_literal: true
shared_examples 'close quick action' do |issuable_type|
+ include Spec::Support::Helpers::Features::NotesHelpers
+
before do
project.add_maintainer(maintainer)
gitlab_sign_in(maintainer)
@@ -76,10 +78,7 @@ shared_examples 'close quick action' do |issuable_type|
it 'explains close quick action' do
visit public_send("project_#{issuable_type}_path", project, issuable)
- page.within('.js-main-target-form') do
- fill_in 'note[note]', with: "this is done, close\n/close"
- click_on 'Preview'
-
+ preview_note("this is done, close\n/close") do
expect(page).not_to have_content '/close'
expect(page).to have_content 'this is done, close'
expect(page).to have_content "Closes this #{issuable_type.to_s.humanize.downcase}."
diff --git a/spec/uploaders/import_export_uploader_spec.rb b/spec/uploaders/import_export_uploader_spec.rb
index 825c1cabc14..2dea48e3a88 100644
--- a/spec/uploaders/import_export_uploader_spec.rb
+++ b/spec/uploaders/import_export_uploader_spec.rb
@@ -3,9 +3,18 @@ require 'spec_helper'
describe ImportExportUploader do
let(:model) { build_stubbed(:import_export_upload) }
let(:upload) { create(:upload, model: model) }
+ let(:import_export_upload) { ImportExportUpload.new }
subject { described_class.new(model, :import_file) }
+ context 'local store' do
+ describe '#move_to_store' do
+ it 'returns true' do
+ expect(subject.move_to_store).to be true
+ end
+ end
+ end
+
context "object_store is REMOTE" do
before do
stub_uploads_object_storage
@@ -16,5 +25,28 @@ describe ImportExportUploader do
it_behaves_like 'builds correct paths',
store_dir: %r[import_export_upload/import_file/],
upload_path: %r[import_export_upload/import_file/]
+
+ describe '#move_to_store' do
+ it 'returns false' do
+ expect(subject.move_to_store).to be false
+ end
+ end
+
+ describe 'with an export file directly uploaded' do
+ let(:tempfile) { Tempfile.new(['test', '.gz']) }
+
+ before do
+ stub_uploads_object_storage(described_class, direct_upload: true)
+ import_export_upload.export_file = tempfile
+ end
+
+ it 'cleans up cached file' do
+ cache_dir = File.join(import_export_upload.export_file.cache_path(nil), '*')
+
+ import_export_upload.save!
+
+ expect(Dir[cache_dir]).to be_empty
+ end
+ end
end
end