summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock9
-rw-r--r--app/assets/javascripts/contextual_sidebar.js3
-rw-r--r--app/assets/javascripts/diffs/components/app.vue12
-rw-r--r--app/assets/javascripts/diffs/store/actions.js9
-rw-r--r--app/assets/javascripts/fly_out_nav.js3
-rw-r--r--app/assets/javascripts/issuable_bulk_update_actions.js3
-rw-r--r--app/assets/javascripts/issuable_index.js8
-rw-r--r--app/assets/javascripts/issue.js11
-rw-r--r--app/assets/javascripts/issue_status_select.js3
-rw-r--r--app/assets/javascripts/main.js7
-rw-r--r--app/assets/javascripts/merge_request_tabs.js5
-rw-r--r--app/assets/javascripts/milestone.js3
-rw-r--r--app/assets/javascripts/milestone_select.js15
-rw-r--r--app/assets/javascripts/mini_pipeline_graph_dropdown.js3
-rw-r--r--app/assets/javascripts/namespace_select.js3
-rw-r--r--app/assets/javascripts/notes.js42
-rw-r--r--app/assets/stylesheets/framework/common.scss4
-rw-r--r--app/assets/stylesheets/framework/header.scss16
-rw-r--r--app/controllers/admin/application_settings_controller.rb7
-rw-r--r--app/controllers/concerns/project_unauthorized.rb14
-rw-r--r--app/controllers/concerns/routable_actions.rb18
-rw-r--r--app/controllers/projects/application_controller.rb3
-rw-r--r--app/controllers/projects/clusters/applications_controller.rb4
-rw-r--r--app/controllers/projects/clusters_controller.rb4
-rw-r--r--app/controllers/projects/serverless/functions_controller.rb2
-rw-r--r--app/controllers/uploads_controller.rb2
-rw-r--r--app/helpers/dashboard_helper.rb4
-rw-r--r--app/helpers/emails_helper.rb22
-rw-r--r--app/helpers/events_helper.rb12
-rw-r--r--app/helpers/nav_helper.rb8
-rw-r--r--app/helpers/projects_helper.rb4
-rw-r--r--app/mailers/devise_mailer.rb1
-rw-r--r--app/mailers/emails/issues.rb7
-rw-r--r--app/mailers/emails/merge_requests.rb4
-rw-r--r--app/models/active_session.rb49
-rw-r--r--app/models/application_record.rb4
-rw-r--r--app/models/ci/job_artifact.rb2
-rw-r--r--app/models/event.rb54
-rw-r--r--app/models/members/group_member.rb2
-rw-r--r--app/models/project.rb9
-rw-r--r--app/models/push_event.rb2
-rw-r--r--app/models/repository.rb4
-rw-r--r--app/services/concerns/users/participable_service.rb2
-rw-r--r--app/services/issues/close_service.rb13
-rw-r--r--app/services/members/destroy_service.rb2
-rw-r--r--app/services/notification_service.rb8
-rw-r--r--app/uploaders/attachment_uploader.rb2
-rw-r--r--app/uploaders/avatar_uploader.rb2
-rw-r--r--app/uploaders/personal_file_uploader.rb2
-rw-r--r--app/views/admin/application_settings/_pages.html.haml5
-rw-r--r--app/views/admin/health_check/show.html.haml2
-rw-r--r--app/views/dashboard/projects/_zero_authorized_projects.html.haml15
-rw-r--r--app/views/devise/shared/_signup_box.html.haml1
-rw-r--r--app/views/devise/shared/_tabs_ldap.html.haml1
-rw-r--r--app/views/events/_event.html.haml6
-rw-r--r--app/views/help/index.html.haml3
-rw-r--r--app/views/layouts/_head.html.haml1
-rw-r--r--app/views/layouts/_mailer.html.haml2
-rw-r--r--app/views/layouts/_page.html.haml1
-rw-r--r--app/views/layouts/devise.html.haml3
-rw-r--r--app/views/layouts/header/_default.html.haml5
-rw-r--r--app/views/layouts/header/_help_dropdown.html.haml1
-rw-r--r--app/views/layouts/mailer.text.erb1
-rw-r--r--app/views/layouts/nav/_dashboard.html.haml1
-rw-r--r--app/views/layouts/nav/sidebar/_admin.html.haml9
-rw-r--r--app/views/layouts/nav/sidebar/_group.html.haml3
-rw-r--r--app/views/layouts/nav/sidebar/_profile.html.haml4
-rw-r--r--app/views/layouts/nav/sidebar/_project.html.haml6
-rw-r--r--app/views/layouts/notify.html.haml2
-rw-r--r--app/views/layouts/notify.text.erb1
-rw-r--r--app/views/notify/closed_issue_email.html.haml2
-rw-r--r--app/views/notify/closed_issue_email.text.haml2
-rw-r--r--app/views/projects/branches/_branch.html.haml2
-rw-r--r--app/views/projects/branches/index.html.haml3
-rw-r--r--app/views/projects/project_members/_new_project_member.html.haml3
-rw-r--r--app/views/shared/_clone_panel.html.haml5
-rw-r--r--app/views/shared/members/_member.html.haml2
-rw-r--r--app/views/users/calendar_activities.html.haml2
-rw-r--r--app/workers/process_commit_worker.rb2
-rw-r--r--changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml5
-rw-r--r--changelogs/unreleased/61550-next-badge.yml5
-rw-r--r--changelogs/unreleased/61606-support-string-piwik-website-ids.yml5
-rw-r--r--changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml5
-rw-r--r--changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml5
-rw-r--r--changelogs/unreleased/diff-whitespace-setting-changes.yml5
-rw-r--r--changelogs/unreleased/fix-project-visibility-level-validation.yml5
-rw-r--r--changelogs/unreleased/include-ee-fixtures.yml5
-rw-r--r--changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml5
-rw-r--r--changelogs/unreleased/sh-fix-rebase-error-clearing.yml5
-rw-r--r--changelogs/unreleased/sh-update-process-mem.yml5
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/01_secret_token.rb11
-rw-r--r--config/initializers/seed_fu.rb4
-rw-r--r--config/karma.config.js2
-rw-r--r--config/routes/admin.rb1
-rw-r--r--doc/administration/geo/disaster_recovery/background_verification.md36
-rw-r--r--doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.pngbin0 -> 202130 bytes
-rw-r--r--doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.pngbin0 -> 85997 bytes
-rw-r--r--doc/administration/high_availability/nfs.md5
-rw-r--r--doc/api/repository_files.md2
-rw-r--r--doc/ci/examples/test-and-deploy-python-application-to-heroku.md9
-rw-r--r--doc/ci/introduction/index.md2
-rw-r--r--doc/ci/services/mysql.md4
-rw-r--r--doc/ci/yaml/README.md6
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/architecture.md86
-rw-r--r--doc/development/documentation/index.md56
-rw-r--r--doc/development/fe_guide/graphql.md8
-rw-r--r--doc/development/fe_guide/vuex.md4
-rw-r--r--doc/development/go_guide/index.md4
-rw-r--r--doc/development/session.md65
-rw-r--r--doc/development/testing_guide/frontend_testing.md183
-rw-r--r--doc/install/installation.md30
-rw-r--r--doc/integration/elasticsearch.md2
-rw-r--r--doc/integration/salesforce.md8
-rw-r--r--doc/update/patch_versions.md2
-rw-r--r--doc/update/upgrading_from_source.md16
-rw-r--r--doc/user/application_security/dast/index.md2
-rw-r--r--doc/user/group/custom_project_templates.md2
-rw-r--r--doc/user/img/mermaid_diagram_render_gfm.pngbin2202 -> 0 bytes
-rw-r--r--doc/user/markdown.md13
-rw-r--r--doc/user/permissions.md2
-rw-r--r--doc/user/project/integrations/webhooks.md20
-rw-r--r--doc/user/project/new_ci_build_permissions_model.md4
-rw-r--r--lib/api/entities.rb2
-rw-r--r--lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml1
-rw-r--r--lib/gitlab/lets_encrypt/challenge.rb17
-rw-r--r--lib/gitlab/lets_encrypt/client.rb74
-rw-r--r--lib/gitlab/lets_encrypt/order.rb23
-rw-r--r--lib/gitlab/sentry.rb15
-rw-r--r--lib/tasks/gitlab/features.rake2
-rw-r--r--locale/gitlab.pot56
-rw-r--r--package.json4
-rw-r--r--qa/README.md6
-rw-r--r--qa/docs/BEST_PRACTICES.md38
-rw-r--r--qa/docs/GUIDELINES.md (renamed from qa/STYLE_GUIDE.md)2
-rw-r--r--qa/docs/WRITING_TESTS_FROM_SCRATCH.md380
-rw-r--r--qa/qa/page/base.rb8
-rw-r--r--qa/qa/page/project/branches/show.rb16
-rw-r--r--qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb6
-rw-r--r--qa/qa/support/page/logging.rb22
-rw-r--r--qa/qa/support/waiter.rb4
-rw-r--r--qa/spec/page/logging_spec.rb9
-rw-r--r--spec/controllers/concerns/project_unauthorized_spec.rb2
-rw-r--r--spec/controllers/concerns/routable_actions_spec.rb156
-rw-r--r--spec/controllers/concerns/send_file_upload_spec.rb3
-rw-r--r--spec/factories/uploads.rb2
-rw-r--r--spec/frontend/environment.js10
-rw-r--r--spec/frontend/test_setup.js9
-rw-r--r--spec/helpers/dashboard_helper_spec.rb6
-rw-r--r--spec/helpers/emails_helper_spec.rb39
-rw-r--r--spec/helpers/nav_helper_spec.rb12
-rw-r--r--spec/helpers/projects_helper_spec.rb20
-rw-r--r--spec/javascripts/diffs/store/actions_spec.js30
-rw-r--r--spec/javascripts/fly_out_nav_spec.js5
-rw-r--r--spec/lib/gitlab/favicon_spec.rb2
-rw-r--r--spec/lib/gitlab/lets_encrypt/challenge_spec.rb29
-rw-r--r--spec/lib/gitlab/lets_encrypt/client_spec.rb120
-rw-r--r--spec/lib/gitlab/lets_encrypt/order_spec.rb39
-rw-r--r--spec/lib/gitlab/sentry_spec.rb5
-rw-r--r--spec/models/active_session_spec.rb46
-rw-r--r--spec/models/application_record_spec.rb6
-rw-r--r--spec/models/event_spec.rb2
-rw-r--r--spec/models/project_spec.rb7
-rw-r--r--spec/models/push_event_spec.rb4
-rw-r--r--spec/requests/api/helpers_spec.rb5
-rw-r--r--spec/services/issues/close_service_spec.rb44
-rw-r--r--spec/services/merge_requests/rebase_service_spec.rb26
-rw-r--r--spec/support/helpers/features/notes_helpers.rb10
-rw-r--r--spec/support/helpers/graphql_helpers.rb4
-rw-r--r--spec/support/helpers/lets_encrypt_helpers.rb19
-rw-r--r--spec/uploaders/object_storage_spec.rb2
173 files changed, 2191 insertions, 335 deletions
diff --git a/Gemfile b/Gemfile
index 19432758b34..d20dbe7c2fd 100644
--- a/Gemfile
+++ b/Gemfile
@@ -60,6 +60,8 @@ gem 'u2f', '~> 0.2.1'
# GitLab Pages
gem 'validates_hostname', '~> 1.0.6'
gem 'rubyzip', '~> 1.2.2', require: 'zip'
+# GitLab Pages letsencrypt support
+gem 'acme-client', '~> 2.0.2'
# Browser detection
gem 'browser', '~> 2.5'
diff --git a/Gemfile.lock b/Gemfile.lock
index 3dd1adb5f63..482352e59d5 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -4,6 +4,8 @@ GEM
RedCloth (4.3.2)
abstract_type (0.0.7)
ace-rails-ap (4.1.2)
+ acme-client (2.0.2)
+ faraday (~> 0.9, >= 0.9.1)
actioncable (5.1.7)
actionpack (= 5.1.7)
nio4r (~> 2.0)
@@ -272,7 +274,7 @@ GEM
ruby-progressbar (~> 1.4)
gemojione (3.3.0)
json
- get_process_mem (0.2.0)
+ get_process_mem (0.2.3)
gettext (3.2.9)
locale (>= 2.0.5)
text (>= 1.3.0)
@@ -812,7 +814,7 @@ GEM
ruby-progressbar (1.10.0)
ruby-saml (1.7.2)
nokogiri (>= 1.5.10)
- ruby_parser (3.11.0)
+ ruby_parser (3.13.1)
sexp_processor (~> 4.9)
rubyntlm (0.6.2)
rubypants (0.2.0)
@@ -852,7 +854,7 @@ GEM
sentry-raven (2.9.0)
faraday (>= 0.7.6, < 1.0)
settingslogic (2.0.9)
- sexp_processor (4.11.0)
+ sexp_processor (4.12.0)
sham_rack (1.3.6)
rack
shoulda-matchers (3.1.2)
@@ -998,6 +1000,7 @@ PLATFORMS
DEPENDENCIES
RedCloth (~> 4.3.2)
ace-rails-ap (~> 4.1.0)
+ acme-client (~> 2.0.2)
activerecord_sane_schema_dumper (= 1.0)
acts-as-taggable-on (~> 6.0)
addressable (~> 2.5.2)
diff --git a/app/assets/javascripts/contextual_sidebar.js b/app/assets/javascripts/contextual_sidebar.js
index 03dea1ec0a5..b62ec8a651b 100644
--- a/app/assets/javascripts/contextual_sidebar.js
+++ b/app/assets/javascripts/contextual_sidebar.js
@@ -8,6 +8,8 @@ import { parseBoolean } from '~/lib/utils/common_utils';
// https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/24555#note_134136110
const NAV_SIDEBAR_BREAKPOINT = 1200;
+export const SIDEBAR_COLLAPSED_CLASS = 'js-sidebar-collapsed';
+
export default class ContextualSidebar {
constructor() {
this.initDomElements();
@@ -62,6 +64,7 @@ export default class ContextualSidebar {
const breakpoint = bp.getBreakpointSize();
const dbp = ContextualSidebar.isDesktopBreakpoint();
+ this.$sidebar.toggleClass(SIDEBAR_COLLAPSED_CLASS, !show);
this.$sidebar.toggleClass('sidebar-expanded-mobile', !dbp ? show : false);
this.$overlay.toggleClass(
'mobile-nav-open',
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 0ed4dcdcd81..11d6672cacf 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -157,10 +157,12 @@ export default {
this.adjustView();
eventHub.$once('fetchedNotesData', this.setDiscussions);
eventHub.$once('fetchDiffData', this.fetchData);
+ eventHub.$on('refetchDiffData', this.refetchDiffData);
this.CENTERED_LIMITED_CONTAINER_CLASSES = CENTERED_LIMITED_CONTAINER_CLASSES;
},
beforeDestroy() {
eventHub.$off('fetchDiffData', this.fetchData);
+ eventHub.$off('refetchDiffData', this.refetchDiffData);
this.removeEventListeners();
},
methods: {
@@ -175,10 +177,16 @@ export default {
'scrollToFile',
'toggleShowTreeList',
]),
- fetchData() {
+ refetchDiffData() {
+ this.assignedDiscussions = false;
+ this.fetchData(false);
+ },
+ fetchData(toggleTree = true) {
this.fetchDiffFiles()
.then(() => {
- this.hideTreeListIfJustOneFile();
+ if (toggleTree) {
+ this.hideTreeListIfJustOneFile();
+ }
requestIdleCallback(
() => {
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 386d08aed2b..35297b7c264 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -52,7 +52,7 @@ export const fetchDiffFiles = ({ state, commit }) => {
});
return axios
- .get(state.endpoint, { params: { w: state.showWhitespace ? null : '1' } })
+ .get(mergeUrlParams({ w: state.showWhitespace ? '0' : '1' }, state.endpoint))
.then(res => {
commit(types.SET_LOADING, false);
commit(types.SET_MERGE_REQUEST_DIFFS, res.data.merge_request_diffs || []);
@@ -125,7 +125,8 @@ export const startRenderDiffsQueue = ({ state, commit }) => {
new Promise(resolve => {
const nextFile = state.diffFiles.find(
file =>
- !file.renderIt && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text),
+ !file.renderIt &&
+ (file.viewer && (!file.viewer.collapsed || !file.viewer.name === diffViewerModes.text)),
);
if (nextFile) {
@@ -315,8 +316,10 @@ export const setShowWhitespace = ({ commit }, { showWhitespace, pushState = fals
localStorage.setItem(WHITESPACE_STORAGE_KEY, showWhitespace);
if (pushState) {
- historyPushState(showWhitespace ? '?w=0' : '?w=1');
+ historyPushState(mergeUrlParams({ w: showWhitespace ? '0' : '1' }, window.location.href));
}
+
+ eventHub.$emit('refetchDiffData');
};
export const toggleFileFinder = ({ commit }, visible) => {
diff --git a/app/assets/javascripts/fly_out_nav.js b/app/assets/javascripts/fly_out_nav.js
index 2b6af9060d1..2566ed6b47c 100644
--- a/app/assets/javascripts/fly_out_nav.js
+++ b/app/assets/javascripts/fly_out_nav.js
@@ -1,4 +1,5 @@
import bp from './breakpoints';
+import { SIDEBAR_COLLAPSED_CLASS } from './contextual_sidebar';
const HIDE_INTERVAL_TIMEOUT = 300;
const IS_OVER_CLASS = 'is-over';
@@ -29,7 +30,7 @@ const setHeaderHeight = () => {
};
export const isSidebarCollapsed = () =>
- sidebar && sidebar.classList.contains('sidebar-collapsed-desktop');
+ sidebar && sidebar.classList.contains(SIDEBAR_COLLAPSED_CLASS);
export const canShowActiveSubItems = el => {
if (el.classList.contains('active') && !isSidebarCollapsed()) {
diff --git a/app/assets/javascripts/issuable_bulk_update_actions.js b/app/assets/javascripts/issuable_bulk_update_actions.js
index ccbe591a63e..bc9d7fcf30d 100644
--- a/app/assets/javascripts/issuable_bulk_update_actions.js
+++ b/app/assets/javascripts/issuable_bulk_update_actions.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import _ from 'underscore';
import axios from './lib/utils/axios_utils';
import Flash from './flash';
+import { __ } from './locale';
export default {
init({ container, form, issues, prefixId } = {}) {
@@ -32,7 +33,7 @@ export default {
onFormSubmitFailure() {
this.form.find('[type="submit"]').enable();
- return new Flash('Issue update failed');
+ return new Flash(__('Issue update failed'));
},
getSelectedIssues() {
diff --git a/app/assets/javascripts/issuable_index.js b/app/assets/javascripts/issuable_index.js
index ffcbd7cf28c..f51c7a2f990 100644
--- a/app/assets/javascripts/issuable_index.js
+++ b/app/assets/javascripts/issuable_index.js
@@ -1,7 +1,7 @@
import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
-import { __ } from './locale';
+import { s__, __ } from './locale';
import IssuableBulkUpdateSidebar from './issuable_bulk_update_sidebar';
import IssuableBulkUpdateActions from './issuable_bulk_update_actions';
@@ -29,7 +29,7 @@ export default class IssuableIndex {
$resetToken.on('click', e => {
e.preventDefault();
- $resetToken.text('resetting...');
+ $resetToken.text(s__('EmailToken|resetting...'));
axios
.put($resetToken.attr('href'))
@@ -38,12 +38,12 @@ export default class IssuableIndex {
.val(data.new_address)
.focus();
- $resetToken.text('reset it');
+ $resetToken.text(s__('EmailToken|reset it'));
})
.catch(() => {
flash(__('There was an error when reseting email token.'));
- $resetToken.text('reset it');
+ $resetToken.text(s__('EmailToken|reset it'));
});
});
}
diff --git a/app/assets/javascripts/issue.js b/app/assets/javascripts/issue.js
index cd1afb6ba83..db4607ca58d 100644
--- a/app/assets/javascripts/issue.js
+++ b/app/assets/javascripts/issue.js
@@ -7,6 +7,7 @@ import flash from './flash';
import TaskList from './task_list';
import CreateMergeRequestDropdown from './create_merge_request_dropdown';
import IssuablesHelper from './helpers/issuables_helper';
+import { __ } from './locale';
export default class Issue {
constructor() {
@@ -44,7 +45,11 @@ export default class Issue {
* @param {Array} data
* @param {String} issueFailMessage
*/
- updateTopState(isClosed, data, issueFailMessage = 'Unable to update this issue at this time.') {
+ updateTopState(
+ isClosed,
+ data,
+ issueFailMessage = __('Unable to update this issue at this time.'),
+ ) {
if ('id' in data) {
const isClosedBadge = $('div.status-box-issue-closed');
const isOpenBadge = $('div.status-box-open');
@@ -81,7 +86,7 @@ export default class Issue {
}
initIssueBtnEventListeners() {
- const issueFailMessage = 'Unable to update this issue at this time.';
+ const issueFailMessage = __('Unable to update this issue at this time.');
return $(document).on(
'click',
@@ -152,6 +157,6 @@ export default class Issue {
$container.html(data.html);
}
})
- .catch(() => flash('Failed to load related branches'));
+ .catch(() => flash(__('Failed to load related branches')));
}
}
diff --git a/app/assets/javascripts/issue_status_select.js b/app/assets/javascripts/issue_status_select.js
index c14803c80e7..75edff41a89 100644
--- a/app/assets/javascripts/issue_status_select.js
+++ b/app/assets/javascripts/issue_status_select.js
@@ -1,4 +1,5 @@
import $ from 'jquery';
+import { __ } from './locale';
export default function issueStatusSelect() {
$('.js-issue-status').each((i, el) => {
@@ -7,7 +8,7 @@ export default function issueStatusSelect() {
selectable: true,
fieldName,
toggleLabel(selected, element, instance) {
- let label = 'Author';
+ let label = __('Author');
const $item = instance.dropdown.find('.is-active');
if ($item.length) {
label = $item.text();
diff --git a/app/assets/javascripts/main.js b/app/assets/javascripts/main.js
index b503c746801..1af21715551 100644
--- a/app/assets/javascripts/main.js
+++ b/app/assets/javascripts/main.js
@@ -31,6 +31,7 @@ import initPerformanceBar from './performance_bar';
import initSearchAutocomplete from './search_autocomplete';
import GlFieldErrors from './gl_field_errors';
import initUserPopovers from './user_popovers';
+import { __ } from './locale';
// expose jQuery as global (TODO: remove these)
window.jQuery = jQuery;
@@ -147,7 +148,7 @@ function deferredInitialisation() {
const canaryBadge = document.querySelector('.js-canary-badge');
const canaryLink = document.querySelector('.js-canary-link');
if (canaryBadge) {
- canaryBadge.classList.remove('hidden');
+ canaryBadge.classList.add('hidden');
}
if (canaryLink) {
canaryLink.classList.add('hidden');
@@ -219,9 +220,9 @@ document.addEventListener('DOMContentLoaded', () => {
const ref = xhrObj.status;
if (ref === 401) {
- Flash('You need to be logged in.');
+ Flash(__('You need to be logged in.'));
} else if (ref === 404 || ref === 500) {
- Flash('Something went wrong on our end.');
+ Flash(__('Something went wrong on our end.'));
}
});
diff --git a/app/assets/javascripts/merge_request_tabs.js b/app/assets/javascripts/merge_request_tabs.js
index 509f19e6f00..e5cf43e8289 100644
--- a/app/assets/javascripts/merge_request_tabs.js
+++ b/app/assets/javascripts/merge_request_tabs.js
@@ -21,6 +21,7 @@ import { localTimeAgo } from './lib/utils/datetime_utility';
import syntaxHighlight from './syntax_highlight';
import Notes from './notes';
import { polyfillSticky } from './lib/utils/sticky';
+import { __ } from './locale';
// MergeRequestTabs
//
@@ -326,7 +327,7 @@ export default class MergeRequestTabs {
})
.catch(() => {
this.toggleLoading(false);
- flash('An error occurred while fetching this tab.');
+ flash(__('An error occurred while fetching this tab.'));
});
}
@@ -416,7 +417,7 @@ export default class MergeRequestTabs {
})
.catch(() => {
this.toggleLoading(false);
- flash('An error occurred while fetching this tab.');
+ flash(__('An error occurred while fetching this tab.'));
});
}
diff --git a/app/assets/javascripts/milestone.js b/app/assets/javascripts/milestone.js
index f211632cf24..6aaba4e7c74 100644
--- a/app/assets/javascripts/milestone.js
+++ b/app/assets/javascripts/milestone.js
@@ -2,6 +2,7 @@ import $ from 'jquery';
import axios from './lib/utils/axios_utils';
import flash from './flash';
import { mouseenter, debouncedMouseleave, togglePopover } from './shared/popover';
+import { __ } from './locale';
export default class Milestone {
constructor() {
@@ -42,7 +43,7 @@ export default class Milestone {
$(tabElId).html(data.html);
$target.addClass('is-loaded');
})
- .catch(() => flash('Error loading milestone tab'));
+ .catch(() => flash(__('Error loading milestone tab')));
}
}
diff --git a/app/assets/javascripts/milestone_select.js b/app/assets/javascripts/milestone_select.js
index 75c18a9b6a0..43949d5cc86 100644
--- a/app/assets/javascripts/milestone_select.js
+++ b/app/assets/javascripts/milestone_select.js
@@ -56,14 +56,15 @@ export default class MilestoneSelect {
const $value = $block.find('.value');
const $loading = $block.find('.block-loading').fadeOut();
selectedMilestoneDefault = showAny ? '' : null;
- selectedMilestoneDefault = showNo && defaultNo ? 'No Milestone' : selectedMilestoneDefault;
+ selectedMilestoneDefault =
+ showNo && defaultNo ? __('No Milestone') : selectedMilestoneDefault;
selectedMilestone = $dropdown.data('selected') || selectedMilestoneDefault;
if (issueUpdateURL) {
milestoneLinkTemplate = _.template(
'<a href="<%- web_url %>" class="bold has-tooltip" data-container="body" title="<%- remaining %>"><%- title %></a>',
);
- milestoneLinkNoneTemplate = '<span class="no-value">None</span>';
+ milestoneLinkNoneTemplate = `<span class="no-value">${__('None')}</span>`;
}
return $dropdown.glDropdown({
showMenuAbove: showMenuAbove,
@@ -74,28 +75,28 @@ export default class MilestoneSelect {
extraOptions.push({
id: null,
name: null,
- title: 'Any Milestone',
+ title: __('Any Milestone'),
});
}
if (showNo) {
extraOptions.push({
id: -1,
- name: 'No Milestone',
- title: 'No Milestone',
+ name: __('No Milestone'),
+ title: __('No Milestone'),
});
}
if (showUpcoming) {
extraOptions.push({
id: -2,
name: '#upcoming',
- title: 'Upcoming',
+ title: __('Upcoming'),
});
}
if (showStarted) {
extraOptions.push({
id: -3,
name: '#started',
- title: 'Started',
+ title: __('Started'),
});
}
if (extraOptions.length) {
diff --git a/app/assets/javascripts/mini_pipeline_graph_dropdown.js b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
index 81ab9d8be4b..b39ad764f01 100644
--- a/app/assets/javascripts/mini_pipeline_graph_dropdown.js
+++ b/app/assets/javascripts/mini_pipeline_graph_dropdown.js
@@ -1,6 +1,7 @@
import $ from 'jquery';
import flash from './flash';
import axios from './lib/utils/axios_utils';
+import { __ } from './locale';
/**
* In each pipelines table we have a mini pipeline graph for each pipeline.
@@ -98,7 +99,7 @@ export default class MiniPipelineGraph {
) {
$(button).dropdown('toggle');
}
- flash('An error occurred while fetching the builds.', 'alert');
+ flash(__('An error occurred while fetching the builds.'), 'alert');
});
}
diff --git a/app/assets/javascripts/namespace_select.js b/app/assets/javascripts/namespace_select.js
index ee1a5274ff7..03d349ac714 100644
--- a/app/assets/javascripts/namespace_select.js
+++ b/app/assets/javascripts/namespace_select.js
@@ -4,6 +4,7 @@ import $ from 'jquery';
import Api from './api';
import { mergeUrlParams } from './lib/utils/url_utility';
import { parseBoolean } from '~/lib/utils/common_utils';
+import { __ } from './locale';
export default class NamespaceSelect {
constructor(opts) {
@@ -29,7 +30,7 @@ export default class NamespaceSelect {
return Api.namespaces(term, function(namespaces) {
if (isFilter) {
const anyNamespace = {
- text: 'Any namespace',
+ text: __('Any namespace'),
id: null,
};
namespaces.unshift(anyNamespace);
diff --git a/app/assets/javascripts/notes.js b/app/assets/javascripts/notes.js
index 36725e22365..d03f4508fb8 100644
--- a/app/assets/javascripts/notes.js
+++ b/app/assets/javascripts/notes.js
@@ -35,6 +35,7 @@ import {
} from './lib/utils/common_utils';
import imageDiffHelper from './image_diff/helpers/index';
import { localTimeAgo } from './lib/utils/datetime_utility';
+import { sprintf, s__, __ } from './locale';
window.autosize = Autosize;
@@ -253,7 +254,7 @@ export default class Notes {
discussionNoteForm = $textarea.closest('.js-discussion-note-form');
if (discussionNoteForm.length) {
if ($textarea.val() !== '') {
- if (!window.confirm('Are you sure you want to cancel creating this comment?')) {
+ if (!window.confirm(__('Are you sure you want to cancel creating this comment?'))) {
return;
}
}
@@ -265,7 +266,7 @@ export default class Notes {
originalText = $textarea.closest('form').data('originalNote');
newText = $textarea.val();
if (originalText !== newText) {
- if (!window.confirm('Are you sure you want to cancel editing this comment?')) {
+ if (!window.confirm(__('Are you sure you want to cancel editing this comment?'))) {
return;
}
}
@@ -636,7 +637,7 @@ export default class Notes {
this.glForm = new GLForm(form, enableGFM);
textarea = form.find('.js-note-text');
key = [
- 'Note',
+ s__('NoteForm|Note'),
form.find('#note_noteable_type').val(),
form.find('#note_noteable_id').val(),
form.find('#note_commit_id').val(),
@@ -670,7 +671,9 @@ export default class Notes {
formParentTimeline = $form.closest('.discussion-notes').find('.notes');
}
return this.addFlash(
- 'Your comment could not be submitted! Please check your network connection and try again.',
+ __(
+ 'Your comment could not be submitted! Please check your network connection and try again.',
+ ),
'alert',
formParentTimeline.get(0),
);
@@ -679,7 +682,7 @@ export default class Notes {
updateNoteError($parentTimeline) {
// eslint-disable-next-line no-new
new Flash(
- 'Your comment could not be updated! Please check your network connection and try again.',
+ __('Your comment could not be updated! Please check your network connection and try again.'),
);
}
@@ -1258,12 +1261,19 @@ export default class Notes {
putConflictEditWarningInPlace(noteEntity, $note) {
if ($note.find('.js-conflict-edit-warning').length === 0) {
+ const open_link = `<a href="#note_${
+ noteEntity.id
+ }" target="_blank" rel="noopener noreferrer">`;
const $alert = $(`<div class="js-conflict-edit-warning alert alert-danger">
- This comment has changed since you started editing, please review the
- <a href="#note_${noteEntity.id}" target="_blank" rel="noopener noreferrer">
- updated comment
- </a>
- to ensure information is not lost
+ ${sprintf(
+ s__(
+ 'Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost',
+ ),
+ {
+ open_link,
+ close_link: '</a>',
+ },
+ )}
</div>`);
$alert.insertAfter($note.find('.note-text'));
}
@@ -1491,13 +1501,15 @@ export default class Notes {
if (executedCommands && executedCommands.length) {
if (executedCommands.length > 1) {
- tempFormContent = 'Applying multiple commands';
+ tempFormContent = __('Applying multiple commands');
} else {
const commandDescription = executedCommands[0].description.toLowerCase();
- tempFormContent = `Applying command to ${commandDescription}`;
+ tempFormContent = sprintf(__('Applying command to %{commandDescription}'), {
+ commandDescription,
+ });
}
} else {
- tempFormContent = 'Applying command';
+ tempFormContent = __('Applying command');
}
return tempFormContent;
@@ -1817,7 +1829,9 @@ export default class Notes {
$editingNote
.find('.note-headline-meta a')
.html(
- '<i class="fa fa-spinner fa-spin" aria-label="Comment is being updated" aria-hidden="true"></i>',
+ `<i class="fa fa-spinner fa-spin" aria-label="${__(
+ 'Comment is being updated',
+ )}" aria-hidden="true"></i>`,
);
// Make request to update comment on server
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 2b499e50ea3..2e38b260f5f 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -439,10 +439,6 @@ img.emoji {
.min-height-0 { min-height: 0; }
-.w-3 { width: #{3 * $grid-size}; }
-
-.h-3 { width: #{3 * $grid-size}; }
-
.svg-w-100 {
svg {
width: 100%;
diff --git a/app/assets/stylesheets/framework/header.scss b/app/assets/stylesheets/framework/header.scss
index a57cd6737f8..1bc597bd4ae 100644
--- a/app/assets/stylesheets/framework/header.scss
+++ b/app/assets/stylesheets/framework/header.scss
@@ -472,6 +472,22 @@
background-color: $blue-500;
}
}
+
+ .canary-badge {
+ .badge {
+ font-size: $gl-font-size-small;
+ line-height: $gl-line-height;
+ padding: 0 $grid-size;
+ }
+
+ &:hover {
+ text-decoration: none;
+
+ .badge {
+ text-decoration: none;
+ }
+ }
+ }
}
@include media-breakpoint-down(xs) {
diff --git a/app/controllers/admin/application_settings_controller.rb b/app/controllers/admin/application_settings_controller.rb
index d445be0eb19..d5bc723aa8c 100644
--- a/app/controllers/admin/application_settings_controller.rb
+++ b/app/controllers/admin/application_settings_controller.rb
@@ -89,6 +89,13 @@ class Admin::ApplicationSettingsController < Admin::ApplicationController
)
end
+ # Getting ToS url requires `directory` api call to Let's Encrypt
+ # which could result in 500 error/slow rendering on settings page
+ # Because of that we use separate controller action
+ def lets_encrypt_terms_of_service
+ redirect_to ::Gitlab::LetsEncrypt.terms_of_service_url
+ end
+
private
def set_application_setting
diff --git a/app/controllers/concerns/project_unauthorized.rb b/app/controllers/concerns/project_unauthorized.rb
index d42363b8b17..7238840440f 100644
--- a/app/controllers/concerns/project_unauthorized.rb
+++ b/app/controllers/concerns/project_unauthorized.rb
@@ -1,10 +1,12 @@
# frozen_string_literal: true
module ProjectUnauthorized
- def project_unauthorized_proc
- lambda do |project|
- if project
- label = project.external_authorization_classification_label
+ module ControllerActions
+ def self.on_routable_not_found
+ lambda do |routable|
+ return unless routable.is_a?(Project)
+
+ label = routable.external_authorization_classification_label
rejection_reason = nil
unless ::Gitlab::ExternalAuthorization.access_allowed?(current_user, label)
@@ -12,9 +14,7 @@ module ProjectUnauthorized
rejection_reason ||= _('External authorization denied access to this project')
end
- if rejection_reason
- access_denied!(rejection_reason)
- end
+ access_denied!(rejection_reason) if rejection_reason
end
end
end
diff --git a/app/controllers/concerns/routable_actions.rb b/app/controllers/concerns/routable_actions.rb
index 5624eb3aa45..ff9b0332c97 100644
--- a/app/controllers/concerns/routable_actions.rb
+++ b/app/controllers/concerns/routable_actions.rb
@@ -3,15 +3,13 @@
module RoutableActions
extend ActiveSupport::Concern
- def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil, not_found_or_authorized_proc: nil)
+ def find_routable!(routable_klass, requested_full_path, extra_authorization_proc: nil)
routable = routable_klass.find_by_full_path(requested_full_path, follow_redirects: request.get?)
if routable_authorized?(routable, extra_authorization_proc)
ensure_canonical_path(routable, requested_full_path)
routable
else
- if not_found_or_authorized_proc
- not_found_or_authorized_proc.call(routable)
- end
+ perform_not_found_actions(routable, not_found_actions)
route_not_found unless performed?
@@ -19,6 +17,18 @@ module RoutableActions
end
end
+ def not_found_actions
+ [ProjectUnauthorized::ControllerActions.on_routable_not_found]
+ end
+
+ def perform_not_found_actions(routable, actions)
+ actions.each do |action|
+ break if performed?
+
+ instance_exec(routable, &action)
+ end
+ end
+
def routable_authorized?(routable, extra_authorization_proc)
return false unless routable
diff --git a/app/controllers/projects/application_controller.rb b/app/controllers/projects/application_controller.rb
index 781eac7f080..80e4f54bbf4 100644
--- a/app/controllers/projects/application_controller.rb
+++ b/app/controllers/projects/application_controller.rb
@@ -3,7 +3,6 @@
class Projects::ApplicationController < ApplicationController
include CookiesHelper
include RoutableActions
- include ProjectUnauthorized
include ChecksCollaboration
skip_before_action :authenticate_user!
@@ -22,7 +21,7 @@ class Projects::ApplicationController < ApplicationController
path = File.join(params[:namespace_id], params[:project_id] || params[:id])
auth_proc = ->(project) { !project.pending_delete? }
- @project = find_routable!(Project, path, extra_authorization_proc: auth_proc, not_found_or_authorized_proc: project_unauthorized_proc)
+ @project = find_routable!(Project, path, extra_authorization_proc: auth_proc)
end
def build_canonical_path(project)
diff --git a/app/controllers/projects/clusters/applications_controller.rb b/app/controllers/projects/clusters/applications_controller.rb
index c7b6218d007..2a04b007304 100644
--- a/app/controllers/projects/clusters/applications_controller.rb
+++ b/app/controllers/projects/clusters/applications_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Projects::Clusters::ApplicationsController < Clusters::ApplicationsController
- include ProjectUnauthorized
-
prepend_before_action :project
private
@@ -12,6 +10,6 @@ class Projects::Clusters::ApplicationsController < Clusters::ApplicationsControl
end
def project
- @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]))
end
end
diff --git a/app/controllers/projects/clusters_controller.rb b/app/controllers/projects/clusters_controller.rb
index feda6deeaa6..cb02581da37 100644
--- a/app/controllers/projects/clusters_controller.rb
+++ b/app/controllers/projects/clusters_controller.rb
@@ -1,8 +1,6 @@
# frozen_string_literal: true
class Projects::ClustersController < Clusters::ClustersController
- include ProjectUnauthorized
-
prepend_before_action :project
before_action :repository
@@ -15,7 +13,7 @@ class Projects::ClustersController < Clusters::ClustersController
end
def project
- @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]), not_found_or_authorized_proc: project_unauthorized_proc)
+ @project ||= find_routable!(Project, File.join(params[:namespace_id], params[:project_id]))
end
def repository
diff --git a/app/controllers/projects/serverless/functions_controller.rb b/app/controllers/projects/serverless/functions_controller.rb
index 8c3d141c888..79030da64d3 100644
--- a/app/controllers/projects/serverless/functions_controller.rb
+++ b/app/controllers/projects/serverless/functions_controller.rb
@@ -3,8 +3,6 @@
module Projects
module Serverless
class FunctionsController < Projects::ApplicationController
- include ProjectUnauthorized
-
before_action :authorize_read_cluster!
def index
diff --git a/app/controllers/uploads_controller.rb b/app/controllers/uploads_controller.rb
index 060b09f015c..5d28635232b 100644
--- a/app/controllers/uploads_controller.rb
+++ b/app/controllers/uploads_controller.rb
@@ -45,7 +45,7 @@ class UploadsController < ApplicationController
when Appearance
true
else
- permission = "read_#{model.class.to_s.underscore}".to_sym
+ permission = "read_#{model.class.underscore}".to_sym
can?(current_user, permission, model)
end
diff --git a/app/helpers/dashboard_helper.rb b/app/helpers/dashboard_helper.rb
index d90ef8903a7..42732eb93dd 100644
--- a/app/helpers/dashboard_helper.rb
+++ b/app/helpers/dashboard_helper.rb
@@ -21,6 +21,10 @@ module DashboardHelper
links.any? { |link| dashboard_nav_link?(link) }
end
+ def has_start_trial?
+ false
+ end
+
private
def get_dashboard_nav_links
diff --git a/app/helpers/emails_helper.rb b/app/helpers/emails_helper.rb
index 96471d15aac..dc0e5511fcf 100644
--- a/app/helpers/emails_helper.rb
+++ b/app/helpers/emails_helper.rb
@@ -91,6 +91,28 @@ module EmailsHelper
].join(';')
end
+ def closure_reason_text(closed_via, format: nil)
+ case closed_via
+ when MergeRequest
+ merge_request = MergeRequest.find(closed_via[:id]).present
+
+ case format
+ when :html
+ " via merge request #{link_to(merge_request.to_reference, merge_request.web_url)}"
+ else
+ # If it's not HTML nor text then assume it's text to be safe
+ " via merge request #{merge_request.to_reference} (#{merge_request.web_url})"
+ end
+ when String
+ # Technically speaking this should be Commit but per
+ # https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/15610#note_163812339
+ # we can't deserialize Commit without custom serializer for ActiveJob
+ " via #{closed_via}"
+ else
+ ""
+ end
+ end
+
# "You are receiving this email because #{reason}"
def notification_reason_text(reason)
string = case reason
diff --git a/app/helpers/events_helper.rb b/app/helpers/events_helper.rb
index 1371e9993b4..e990e425cb6 100644
--- a/app/helpers/events_helper.rb
+++ b/app/helpers/events_helper.rb
@@ -68,7 +68,7 @@ module EventsHelper
end
def event_preposition(event)
- if event.push? || event.commented? || event.target
+ if event.push_action? || event.commented_action? || event.target
"at"
elsif event.milestone?
"in"
@@ -80,11 +80,11 @@ module EventsHelper
words << event.author_name
words << event_action_name(event)
- if event.push?
+ if event.push_action?
words << event.ref_type
words << event.ref_name
words << "at"
- elsif event.commented?
+ elsif event.commented_action?
words << event.note_target_reference
words << "at"
elsif event.milestone?
@@ -121,9 +121,9 @@ module EventsHelper
if event.note_target
event_note_target_url(event)
end
- elsif event.push?
+ elsif event.push_action?
push_event_feed_url(event)
- elsif event.created_project?
+ elsif event.created_project_action?
project_url(event.project)
end
end
@@ -147,7 +147,7 @@ module EventsHelper
def event_feed_summary(event)
if event.issue?
render "events/event_issue", issue: event.issue
- elsif event.push?
+ elsif event.push_action?
render "events/event_push", event: event
elsif event.merge_request?
render "events/event_merge_request", merge_request: event.merge_request
diff --git a/app/helpers/nav_helper.rb b/app/helpers/nav_helper.rb
index 05da5ebdb22..a57ba5f3a4f 100644
--- a/app/helpers/nav_helper.rb
+++ b/app/helpers/nav_helper.rb
@@ -58,6 +58,14 @@ module NavHelper
current_path?('milestones#show')
end
+ def admin_monitoring_nav_links
+ %w(system_info background_jobs logs health_check requests_profiles)
+ end
+
+ def group_issues_sub_menu_items
+ %w(groups#issues labels#index milestones#index boards#index boards#show)
+ end
+
private
def get_header_links
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index 2c43b1a2067..91d15e0e4ea 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -315,6 +315,10 @@ module ProjectsHelper
) % { default_label: default_label }
end
+ def can_import_members?
+ Ability.allowed?(current_user, :admin_project_member, @project)
+ end
+
private
def get_project_nav_tabs(project, current_user)
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 7aa75ee30e6..cbaf53fced1 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -7,6 +7,7 @@ class DeviseMailer < Devise::Mailer
layout 'mailer/devise'
helper EmailsHelper
+ helper ApplicationHelper
protected
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index d2e334fb856..2b046d17122 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -30,8 +30,8 @@ module Emails
end
# rubocop: enable CodeReuse/ActiveRecord
- def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason = nil)
- setup_issue_mail(issue_id, recipient_id)
+ def closed_issue_email(recipient_id, issue_id, updated_by_user_id, reason: nil, closed_via: nil)
+ setup_issue_mail(issue_id, recipient_id, closed_via: closed_via)
@updated_by = User.find(updated_by_user_id)
mail_answer_thread(@issue, issue_thread_options(updated_by_user_id, recipient_id, reason))
@@ -91,10 +91,11 @@ module Emails
private
- def setup_issue_mail(issue_id, recipient_id)
+ def setup_issue_mail(issue_id, recipient_id, closed_via: nil)
@issue = Issue.find(issue_id)
@project = @issue.project
@target_url = project_issue_url(@project, @issue)
+ @closed_via = closed_via
@sent_notification = SentNotification.record(@issue, recipient_id, reply_key)
end
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 63148831a24..fc6ed695675 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -58,14 +58,14 @@ module Emails
}))
end
- def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ def closed_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil)
setup_merge_request_mail(merge_request_id, recipient_id)
@updated_by = User.find(updated_by_user_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
end
- def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason = nil)
+ def merged_merge_request_email(recipient_id, merge_request_id, updated_by_user_id, reason: nil, closed_via: nil)
setup_merge_request_mail(merge_request_id, recipient_id)
mail_answer_thread(@merge_request, merge_request_thread_options(updated_by_user_id, recipient_id, reason))
diff --git a/app/models/active_session.rb b/app/models/active_session.rb
index 1e01f1d17e6..f355b02c428 100644
--- a/app/models/active_session.rb
+++ b/app/models/active_session.rb
@@ -53,7 +53,7 @@ class ActiveSession
def self.list(user)
Gitlab::Redis::SharedState.with do |redis|
- cleaned_up_lookup_entries(redis, user.id).map do |entry|
+ cleaned_up_lookup_entries(redis, user).map do |entry|
# rubocop:disable Security/MarshalLoad
Marshal.load(entry)
# rubocop:enable Security/MarshalLoad
@@ -78,7 +78,7 @@ class ActiveSession
def self.cleanup(user)
Gitlab::Redis::SharedState.with do |redis|
- cleaned_up_lookup_entries(redis, user.id)
+ cleaned_up_lookup_entries(redis, user)
end
end
@@ -90,25 +90,52 @@ class ActiveSession
"#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user_id}"
end
- def self.cleaned_up_lookup_entries(redis, user_id)
- lookup_key = lookup_key_name(user_id)
+ def self.list_sessions(user)
+ sessions_from_ids(session_ids_for_user(user))
+ end
- session_ids = redis.smembers(lookup_key)
+ def self.session_ids_for_user(user)
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.smembers(lookup_key_name(user.id))
+ end
+ end
- entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
- return [] if entry_keys.empty?
+ def self.sessions_from_ids(session_ids)
+ return [] if session_ids.empty?
- entries = redis.mget(entry_keys)
+ Gitlab::Redis::SharedState.with do |redis|
+ session_keys = session_ids.map { |session_id| "#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}" }
- session_ids_and_entries = session_ids.zip(entries)
+ redis.mget(session_keys).compact.map do |raw_session|
+ # rubocop:disable Security/MarshalLoad
+ Marshal.load(raw_session)
+ # rubocop:enable Security/MarshalLoad
+ end
+ end
+ end
+
+ def self.raw_active_session_entries(session_ids, user_id)
+ return [] if session_ids.empty?
+
+ Gitlab::Redis::SharedState.with do |redis|
+ entry_keys = session_ids.map { |session_id| key_name(user_id, session_id) }
+
+ redis.mget(entry_keys)
+ end
+ end
+
+ def self.cleaned_up_lookup_entries(redis, user)
+ session_ids = session_ids_for_user(user)
+ entries = raw_active_session_entries(session_ids, user.id)
# remove expired keys.
# only the single key entries are automatically expired by redis, the
# lookup entries in the set need to be removed manually.
+ session_ids_and_entries = session_ids.zip(entries)
session_ids_and_entries.reject { |_session_id, entry| entry }.each do |session_id, _entry|
- redis.srem(lookup_key, session_id)
+ redis.srem(lookup_key_name(user.id), session_id)
end
- session_ids_and_entries.select { |_session_id, entry| entry }.map { |_session_id, entry| entry }
+ entries.compact
end
end
diff --git a/app/models/application_record.rb b/app/models/application_record.rb
index d1d01368972..0979d03f6e6 100644
--- a/app/models/application_record.rb
+++ b/app/models/application_record.rb
@@ -41,4 +41,8 @@ class ApplicationRecord < ActiveRecord::Base
find_or_create_by(*args)
end
end
+
+ def self.underscore
+ Gitlab::SafeRequestStore.fetch("model:#{self}:underscore") { self.to_s.underscore }
+ end
end
diff --git a/app/models/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index f9cf398556d..3beb76ffc2b 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -99,7 +99,7 @@ module Ci
raw: 1,
zip: 2,
gzip: 3
- }
+ }, _suffix: true
# `file_location` indicates where actual files are stored.
# Ideally, actual files should be stored in the same directory, and use the same
diff --git a/app/models/event.rb b/app/models/event.rb
index 593acf5edfe..738080eb584 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -68,7 +68,7 @@ class Event < ApplicationRecord
# Callbacks
after_create :reset_project_activity
- after_create :set_last_repository_updated_at, if: :push?
+ after_create :set_last_repository_updated_at, if: :push_action?
after_create :track_user_interacted_projects
# Scopes
@@ -138,11 +138,11 @@ class Event < ApplicationRecord
# rubocop:disable Metrics/CyclomaticComplexity
# rubocop:disable Metrics/PerceivedComplexity
def visible_to_user?(user = nil)
- if push? || commit_note?
+ if push_action? || commit_note?
Ability.allowed?(user, :download_code, project)
elsif membership_changed?
Ability.allowed?(user, :read_project, project)
- elsif created_project?
+ elsif created_project_action?
Ability.allowed?(user, :read_project, project)
elsif issue? || issue_note?
Ability.allowed?(user, :read_issue, note? ? note_target : target)
@@ -173,56 +173,56 @@ class Event < ApplicationRecord
target.try(:title)
end
- def created?
+ def created_action?
action == CREATED
end
- def push?
+ def push_action?
false
end
- def merged?
+ def merged_action?
action == MERGED
end
- def closed?
+ def closed_action?
action == CLOSED
end
- def reopened?
+ def reopened_action?
action == REOPENED
end
- def joined?
+ def joined_action?
action == JOINED
end
- def left?
+ def left_action?
action == LEFT
end
- def expired?
+ def expired_action?
action == EXPIRED
end
- def destroyed?
+ def destroyed_action?
action == DESTROYED
end
- def commented?
+ def commented_action?
action == COMMENTED
end
def membership_changed?
- joined? || left? || expired?
+ joined_action? || left_action? || expired_action?
end
- def created_project?
- created? && !target && target_type.nil?
+ def created_project_action?
+ created_action? && !target && target_type.nil?
end
def created_target?
- created? && target
+ created_action? && target
end
def milestone?
@@ -258,23 +258,23 @@ class Event < ApplicationRecord
end
def action_name
- if push?
+ if push_action?
push_action_name
- elsif closed?
+ elsif closed_action?
"closed"
- elsif merged?
+ elsif merged_action?
"accepted"
- elsif joined?
+ elsif joined_action?
'joined'
- elsif left?
+ elsif left_action?
'left'
- elsif expired?
+ elsif expired_action?
'removed due to membership expiration from'
- elsif destroyed?
+ elsif destroyed_action?
'destroyed'
- elsif commented?
+ elsif commented_action?
"commented on"
- elsif created_project?
+ elsif created_project_action?
created_project_action_name
else
"opened"
@@ -337,7 +337,7 @@ class Event < ApplicationRecord
end
def body?
- if push?
+ if push_action?
push_with_commits?
elsif note?
true
diff --git a/app/models/members/group_member.rb b/app/models/members/group_member.rb
index b266c61f002..4cba69069bb 100644
--- a/app/models/members/group_member.rb
+++ b/app/models/members/group_member.rb
@@ -12,7 +12,7 @@ class GroupMember < Member
validates :source_type, format: { with: /\ANamespace\z/ }
default_scope { where(source_type: SOURCE_TYPE) }
- scope :in_groups, ->(groups) { where(source_id: groups.select(:id)) }
+ scope :of_groups, ->(groups) { where(source_id: groups.select(:id)) }
scope :count_users_by_group_id, -> { joins(:user).group(:source_id).count }
diff --git a/app/models/project.rb b/app/models/project.rb
index 61d245478ca..ab4da61dcf8 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -56,7 +56,6 @@ class Project < ApplicationRecord
VALID_MIRROR_PROTOCOLS = %w(http https ssh git).freeze
ignore_column :import_status, :import_jid, :import_error
- ignore_column :ci_id
cache_markdown_field :description, pipeline: :description
@@ -337,8 +336,8 @@ class Project < ApplicationRecord
validates :star_count, numericality: { greater_than_or_equal_to: 0 }
validate :check_personal_projects_limit, on: :create
validate :check_repository_path_availability, on: :update, if: ->(project) { project.renamed? }
- validate :visibility_level_allowed_by_group, if: -> { changes.has_key?(:visibility_level) }
- validate :visibility_level_allowed_as_fork, if: -> { changes.has_key?(:visibility_level) }
+ validate :visibility_level_allowed_by_group, if: :should_validate_visibility_level?
+ validate :visibility_level_allowed_as_fork, if: :should_validate_visibility_level?
validate :check_wiki_path_conflict
validate :validate_pages_https_only, if: -> { changes.has_key?(:pages_https_only) }
validates :repository_storage,
@@ -892,6 +891,10 @@ class Project < ApplicationRecord
self.errors.add(:limit_reached, error % { limit: limit })
end
+ def should_validate_visibility_level?
+ new_record? || changes.has_key?(:visibility_level)
+ end
+
def visibility_level_allowed_by_group
return if visibility_level_allowed_by_group?
diff --git a/app/models/push_event.rb b/app/models/push_event.rb
index 9c0267c3140..4698df39730 100644
--- a/app/models/push_event.rb
+++ b/app/models/push_event.rb
@@ -69,7 +69,7 @@ class PushEvent < Event
PUSHED
end
- def push?
+ def push_action?
true
end
diff --git a/app/models/repository.rb b/app/models/repository.rb
index 1c02e68f2f6..f540b00a849 100644
--- a/app/models/repository.rb
+++ b/app/models/repository.rb
@@ -1050,7 +1050,7 @@ class Repository
# To support the full deprecated behaviour, set the
# `rebase_commit_sha` for the merge_request here and return the value
- merge_request.update(rebase_commit_sha: rebase_sha)
+ merge_request.update(rebase_commit_sha: rebase_sha, merge_error: nil)
rebase_sha
end
@@ -1069,7 +1069,7 @@ class Repository
remote_repository: merge_request.target_project.repository.raw,
remote_branch: merge_request.target_branch
) do |commit_id|
- merge_request.update!(rebase_commit_sha: commit_id)
+ merge_request.update!(rebase_commit_sha: commit_id, merge_error: nil)
end
end
end
diff --git a/app/services/concerns/users/participable_service.rb b/app/services/concerns/users/participable_service.rb
index a3cc6014fd3..1c828234f1b 100644
--- a/app/services/concerns/users/participable_service.rb
+++ b/app/services/concerns/users/participable_service.rb
@@ -29,7 +29,7 @@ module Users
def groups
group_counts = GroupMember
- .in_groups(current_user.authorized_groups)
+ .of_groups(current_user.authorized_groups)
.non_request
.count_users_by_group_id
diff --git a/app/services/issues/close_service.rb b/app/services/issues/close_service.rb
index e5cc12e6082..2a19e57a94f 100644
--- a/app/services/issues/close_service.rb
+++ b/app/services/issues/close_service.rb
@@ -7,7 +7,7 @@ module Issues
return issue unless can?(current_user, :update_issue, issue)
close_issue(issue,
- commit: commit,
+ closed_via: commit,
notifications: notifications,
system_note: system_note)
end
@@ -17,9 +17,9 @@ module Issues
#
# The code calling this method is responsible for ensuring that a user is
# allowed to close the given issue.
- def close_issue(issue, commit: nil, notifications: true, system_note: true)
+ def close_issue(issue, closed_via: nil, notifications: true, system_note: true)
if project.jira_tracker? && project.jira_service.active && issue.is_a?(ExternalIssue)
- project.jira_service.close_issue(commit, issue)
+ project.jira_service.close_issue(closed_via, issue)
todo_service.close_issue(issue, current_user)
return issue
end
@@ -27,8 +27,11 @@ module Issues
if project.issues_enabled? && issue.close
issue.update(closed_by: current_user)
event_service.close_issue(issue, current_user)
- create_note(issue, commit) if system_note
- notification_service.async.close_issue(issue, current_user) if notifications
+ create_note(issue, closed_via) if system_note
+
+ closed_via = "commit #{closed_via.id}" if closed_via.is_a?(Commit)
+
+ notification_service.async.close_issue(issue, current_user, closed_via: closed_via) if notifications
todo_service.close_issue(issue, current_user)
execute_hooks(issue, 'close')
invalidate_cache_counts(issue, users: issue.assignees)
diff --git a/app/services/members/destroy_service.rb b/app/services/members/destroy_service.rb
index f9717a9426b..c8d5e563cd8 100644
--- a/app/services/members/destroy_service.rb
+++ b/app/services/members/destroy_service.rb
@@ -45,7 +45,7 @@ module Members
def delete_subgroup_members(member)
groups = member.group.descendants
- GroupMember.in_groups(groups).with_user(member.user).each do |group_member|
+ GroupMember.of_groups(groups).with_user(member.user).each do |group_member|
self.class.new(current_user).execute(group_member, skip_authorization: @skip_auth, skip_subresources: true)
end
end
diff --git a/app/services/notification_service.rb b/app/services/notification_service.rb
index 8d3b569498f..f797c0f11c6 100644
--- a/app/services/notification_service.rb
+++ b/app/services/notification_service.rb
@@ -89,8 +89,8 @@ class NotificationService
# * project team members with notification level higher then Participating
# * users with custom level checked with "close issue"
#
- def close_issue(issue, current_user)
- close_resource_email(issue, current_user, :closed_issue_email)
+ def close_issue(issue, current_user, closed_via: nil)
+ close_resource_email(issue, current_user, :closed_issue_email, closed_via: closed_via)
end
# When we reassign an issue we should send an email to:
@@ -504,7 +504,7 @@ class NotificationService
end
end
- def close_resource_email(target, current_user, method, skip_current_user: true)
+ def close_resource_email(target, current_user, method, skip_current_user: true, closed_via: nil)
action = method == :merged_merge_request_email ? "merge" : "close"
recipients = NotificationRecipientService.build_recipients(
@@ -515,7 +515,7 @@ class NotificationService
)
recipients.each do |recipient|
- mailer.send(method, recipient.user.id, target.id, current_user.id, recipient.reason).deliver_later
+ mailer.send(method, recipient.user.id, target.id, current_user.id, reason: recipient.reason, closed_via: closed_via).deliver_later
end
end
diff --git a/app/uploaders/attachment_uploader.rb b/app/uploaders/attachment_uploader.rb
index 0a166335b4e..b488bba00e9 100644
--- a/app/uploaders/attachment_uploader.rb
+++ b/app/uploaders/attachment_uploader.rb
@@ -9,6 +9,6 @@ class AttachmentUploader < GitlabUploader
private
def dynamic_segment
- File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
+ File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/avatar_uploader.rb b/app/uploaders/avatar_uploader.rb
index c0165759203..9af59b0aceb 100644
--- a/app/uploaders/avatar_uploader.rb
+++ b/app/uploaders/avatar_uploader.rb
@@ -25,6 +25,6 @@ class AvatarUploader < GitlabUploader
private
def dynamic_segment
- File.join(model.class.to_s.underscore, mounted_as.to_s, model.id.to_s)
+ File.join(model.class.underscore, mounted_as.to_s, model.id.to_s)
end
end
diff --git a/app/uploaders/personal_file_uploader.rb b/app/uploaders/personal_file_uploader.rb
index 272837aa6ce..f4697d898af 100644
--- a/app/uploaders/personal_file_uploader.rb
+++ b/app/uploaders/personal_file_uploader.rb
@@ -20,7 +20,7 @@ class PersonalFileUploader < FileUploader
def self.model_path_segment(model)
return 'temp/' unless model
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
def object_store
diff --git a/app/views/admin/application_settings/_pages.html.haml b/app/views/admin/application_settings/_pages.html.haml
index 64e01fa2d00..77795dbf913 100644
--- a/app/views/admin/application_settings/_pages.html.haml
+++ b/app/views/admin/application_settings/_pages.html.haml
@@ -30,8 +30,7 @@
.form-check
= f.check_box :lets_encrypt_terms_of_service_accepted, class: 'form-check-input'
= f.label :lets_encrypt_terms_of_service_accepted, class: 'form-check-label' do
- // Terms of Service should actually be a link, but the best way to get the url is using API
- // So it will be done in later MR
- = _("I have read and agree to the Let's Encrypt Terms of Service")
+ - terms_of_service_link_start = '<a href="%{url}" target="_blank" rel="noopener noreferrer">'.html_safe % { url: lets_encrypt_terms_of_service_admin_application_settings_path }
+ = _("I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}").html_safe % { link_start: terms_of_service_link_start, link_end: '</a>'.html_safe }
= f.submit _('Save changes'), class: "btn btn-success"
diff --git a/app/views/admin/health_check/show.html.haml b/app/views/admin/health_check/show.html.haml
index 0f5e97e288a..ac56e354a4d 100644
--- a/app/views/admin/health_check/show.html.haml
+++ b/app/views/admin/health_check/show.html.haml
@@ -23,7 +23,7 @@
%code= liveness_url(token: Gitlab::CurrentSettings.health_check_access_token)
%li
%code= metrics_url(token: Gitlab::CurrentSettings.health_check_access_token)
-
+ = render_if_exists 'admin/health_check/health_check_url'
%hr
.card
.card-header
diff --git a/app/views/dashboard/projects/_zero_authorized_projects.html.haml b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
index 18a82feb189..8933c5d7227 100644
--- a/app/views/dashboard/projects/_zero_authorized_projects.html.haml
+++ b/app/views/dashboard/projects/_zero_authorized_projects.html.haml
@@ -1,4 +1,4 @@
-.blank-state-parent-container
+.blank-state-parent-container{ class: ('has-start-trial-container' if has_start_trial?) }
.section-container.section-welcome{ class: "#{ 'section-admin-welcome' if current_user.admin? }" }
.container.section-body
.row
@@ -7,7 +7,12 @@
Welcome to GitLab
%p.blank-state-text
Code, test, and deploy together
- - if current_user.admin?
- = render "blank_state_admin_welcome"
- - else
- = render "blank_state_welcome"
+ .blank-state-row
+ %div{ class: ('column-large' if has_start_trial?) }
+ - if current_user.admin?
+ = render "blank_state_admin_welcome"
+ - else
+ = render "blank_state_welcome"
+ - if has_start_trial?
+ .column-small
+ = render_if_exists "blank_state_ee_trial"
diff --git a/app/views/devise/shared/_signup_box.html.haml b/app/views/devise/shared/_signup_box.html.haml
index 9c7ca6ebbd4..427db070253 100644
--- a/app/views/devise/shared/_signup_box.html.haml
+++ b/app/views/devise/shared/_signup_box.html.haml
@@ -29,6 +29,7 @@
- terms_link = link_to s_("I accept the|Terms of Service and Privacy Policy"), terms_path, target: "_blank"
- accept_terms_label = _("I accept the %{terms_link}") % { terms_link: terms_link }
= accept_terms_label.html_safe
+ = render_if_exists 'devise/shared/email_opted_in', f: f
%div
- if Gitlab::Recaptcha.enabled?
= recaptcha_tags
diff --git a/app/views/devise/shared/_tabs_ldap.html.haml b/app/views/devise/shared/_tabs_ldap.html.haml
index aee05b6c81c..b1a9470cf1c 100644
--- a/app/views/devise/shared/_tabs_ldap.html.haml
+++ b/app/views/devise/shared/_tabs_ldap.html.haml
@@ -2,6 +2,7 @@
- if crowd_enabled?
%li.nav-item
= link_to "Crowd", "#crowd", class: "nav-link #{active_when(form_based_auth_provider_has_active_class?(:crowd))}", 'data-toggle' => 'tab'
+ = render_if_exists "devise/shared/kerberos_tab"
- @ldap_servers.each_with_index do |server, i|
%li.nav-item
= link_to server['label'], "##{server['provider_name']}", class: "nav-link #{active_when(i.zero? && form_based_auth_provider_has_active_class?(:ldapmain))} qa-ldap-tab", 'data-toggle' => 'tab'
diff --git a/app/views/events/_event.html.haml b/app/views/events/_event.html.haml
index 2fcb1d1fd2b..222175c818a 100644
--- a/app/views/events/_event.html.haml
+++ b/app/views/events/_event.html.haml
@@ -3,11 +3,11 @@
.event-item-timestamp
#{time_ago_with_tooltip(event.created_at)}
- - if event.created_project?
+ - if event.created_project_action?
= render "events/event/created_project", event: event
- - elsif event.push?
+ - elsif event.push_action?
= render "events/event/push", event: event
- - elsif event.commented?
+ - elsif event.commented_action?
= render "events/event/note", event: event
- else
= render "events/event/common", event: event
diff --git a/app/views/help/index.html.haml b/app/views/help/index.html.haml
index 75e4dc46c9b..50933c7d434 100644
--- a/app/views/help/index.html.haml
+++ b/app/views/help/index.html.haml
@@ -5,8 +5,7 @@
%hr
%h1
- GitLab
- Community Edition
+ = default_brand_title
- if user_signed_in?
%span= link_to_version
= version_status_badge
diff --git a/app/views/layouts/_head.html.haml b/app/views/layouts/_head.html.haml
index 11e83ddfe64..c357207054b 100644
--- a/app/views/layouts/_head.html.haml
+++ b/app/views/layouts/_head.html.haml
@@ -77,3 +77,4 @@
= render 'layouts/google_analytics' if extra_config.has_key?('google_analytics_id')
= render 'layouts/piwik' if extra_config.has_key?('piwik_url') && extra_config.has_key?('piwik_site_id')
+ = render_if_exists 'layouts/snowplow'
diff --git a/app/views/layouts/_mailer.html.haml b/app/views/layouts/_mailer.html.haml
index e13490ed410..6e8294d6adc 100644
--- a/app/views/layouts/_mailer.html.haml
+++ b/app/views/layouts/_mailer.html.haml
@@ -64,6 +64,8 @@
%tbody
= yield
+ = render_if_exists 'layouts/mailer/additional_text'
+
%tr.footer
%td{ style: "font-family:'Helvetica Neue',Helvetica,Arial,sans-serif;padding:25px 0;font-size:13px;line-height:1.6;color:#5c5c5c;" }
%img{ alt: "GitLab", height: "33", src: image_url('mailers/gitlab_footer_logo.gif'), style: "display:block;margin:0 auto 1em;", width: "90" }/
diff --git a/app/views/layouts/_page.html.haml b/app/views/layouts/_page.html.haml
index 26a1f1e119c..006334ade07 100644
--- a/app/views/layouts/_page.html.haml
+++ b/app/views/layouts/_page.html.haml
@@ -5,6 +5,7 @@
= render 'shared/outdated_browser'
.mobile-overlay
.alert-wrapper
+ = render_if_exists "layouts/header/ee_license_banner"
= render "layouts/broadcast"
= render "layouts/header/read_only_banner"
= render "layouts/nav/classification_level_banner"
diff --git a/app/views/layouts/devise.html.haml b/app/views/layouts/devise.html.haml
index 2f3c13aaf6e..f7a561afbb3 100644
--- a/app/views/layouts/devise.html.haml
+++ b/app/views/layouts/devise.html.haml
@@ -26,6 +26,9 @@
- if Gitlab::CurrentSettings.sign_in_text.present?
= markdown_field(Gitlab::CurrentSettings.current_application_settings, :sign_in_text)
+
+ = render_if_exists 'layouts/devise_help_text'
+
.col-sm-5.new-session-forms-container
= yield
diff --git a/app/views/layouts/header/_default.html.haml b/app/views/layouts/header/_default.html.haml
index 724c9976954..1c1d1ea4645 100644
--- a/app/views/layouts/header/_default.html.haml
+++ b/app/views/layouts/header/_default.html.haml
@@ -18,8 +18,9 @@
%span.logo-text.d-none.d-lg-block.prepend-left-8
= logo_text
- if Gitlab.com?
- %span.js-canary-badge.badge.badge-pill.green-badge.align-self-center
- = _('Next')
+ = link_to 'https://next.gitlab.com', class: 'label-link js-canary-badge canary-badge bg-transparent', target: :_blank do
+ %span.color-label.has-tooltip.badge.badge-pill.green-badge
+ = _('Next')
- if current_user
= render "layouts/nav/dashboard"
diff --git a/app/views/layouts/header/_help_dropdown.html.haml b/app/views/layouts/header/_help_dropdown.html.haml
index fbec62b02f8..5643a508ddc 100644
--- a/app/views/layouts/header/_help_dropdown.html.haml
+++ b/app/views/layouts/header/_help_dropdown.html.haml
@@ -8,6 +8,7 @@
= link_to _("Submit feedback"), "https://about.gitlab.com/submit-feedback"
- if current_user_menu?(:help) || current_user_menu?(:settings) || current_user_menu?(:profile)
= render 'shared/user_dropdown_contributing_link'
+ = render_if_exists 'shared/user_dropdown_instance_review'
- if Gitlab.com?
%li.js-canary-link
= link_to _("Switch to GitLab Next"), "https://next.gitlab.com/"
diff --git a/app/views/layouts/mailer.text.erb b/app/views/layouts/mailer.text.erb
index f8032f3262b..1a06ea68bcd 100644
--- a/app/views/layouts/mailer.text.erb
+++ b/app/views/layouts/mailer.text.erb
@@ -4,5 +4,6 @@
-- <%# signature marker %>
<%= _("You're receiving this email because of your account on %{host}.") % { host: Gitlab.config.gitlab.host } %>
+<%= render_if_exists 'layouts/mailer/additional_text' %>
<%= text_footer_message %>
diff --git a/app/views/layouts/nav/_dashboard.html.haml b/app/views/layouts/nav/_dashboard.html.haml
index 5a27237bf76..47710b9e9e5 100644
--- a/app/views/layouts/nav/_dashboard.html.haml
+++ b/app/views/layouts/nav/_dashboard.html.haml
@@ -95,3 +95,4 @@
= link_to sherlock_transactions_path, class: 'admin-icon d-none d-lg-block d-xl-block', title: _('Sherlock Transactions'),
data: {toggle: 'tooltip', placement: 'bottom', container: 'body'} do
= icon('tachometer fw')
+ = render_if_exists 'layouts/nav/geo_primary_node_url'
diff --git a/app/views/layouts/nav/sidebar/_admin.html.haml b/app/views/layouts/nav/sidebar/_admin.html.haml
index 04d67e024ba..83fe871285a 100644
--- a/app/views/layouts/nav/sidebar/_admin.html.haml
+++ b/app/views/layouts/nav/sidebar/_admin.html.haml
@@ -48,7 +48,7 @@
%span
= _('Gitaly Servers')
- = nav_link(controller: %w(system_info background_jobs logs health_check requests_profiles)) do
+ = nav_link(controller: admin_monitoring_nav_links) do
= link_to admin_system_info_path do
.nav-icon-container
= sprite_icon('monitor')
@@ -81,6 +81,7 @@
= link_to admin_requests_profiles_path, title: _('Requests Profiles') do
%span
= _('Requests Profiles')
+ = render_if_exists 'layouts/nav/ee/admin/new_monitoring_sidebar'
= nav_link(controller: :broadcast_messages) do
= link_to admin_broadcast_messages_path do
@@ -132,6 +133,8 @@
= _('Abuse Reports')
%span.badge.badge-pill.count.merge_counter.js-merge-counter.fly-out-badge= number_with_delimiter(AbuseReport.count(:all))
+ = render_if_exists 'layouts/nav/sidebar/licenses_link'
+
- if instance_clusters_enabled?
= nav_link(controller: :clusters) do
= link_to admin_clusters_path do
@@ -158,6 +161,10 @@
%strong.fly-out-top-item-name
= _('Spam Logs')
+ = render_if_exists 'layouts/nav/sidebar/push_rules_link'
+
+ = render_if_exists 'layouts/nav/ee/admin/geo_sidebar'
+
= nav_link(controller: :deploy_keys) do
= link_to admin_deploy_keys_path do
.nav-icon-container
diff --git a/app/views/layouts/nav/sidebar/_group.html.haml b/app/views/layouts/nav/sidebar/_group.html.haml
index c2116ec63dd..0fc5ebbea7e 100644
--- a/app/views/layouts/nav/sidebar/_group.html.haml
+++ b/app/views/layouts/nav/sidebar/_group.html.haml
@@ -1,6 +1,5 @@
- issues_count = group_issues_count(state: 'opened')
- merge_requests_count = group_merge_requests_count(state: 'opened')
-- issues_sub_menu_items = ['groups#issues', 'labels#index', 'milestones#index', 'boards#index', 'boards#show']
.nav-sidebar{ class: ("sidebar-collapsed-desktop" if collapsed_sidebar?) }
.nav-sidebar-inner-scroll
@@ -51,7 +50,7 @@
= render_if_exists "layouts/nav/ee/epic_link", group: @group
- if group_sidebar_link?(:issues)
- = nav_link(path: issues_sub_menu_items) do
+ = nav_link(path: group_issues_sub_menu_items) do
= link_to issues_group_path(@group) do
.nav-icon-container
= sprite_icon('issues')
diff --git a/app/views/layouts/nav/sidebar/_profile.html.haml b/app/views/layouts/nav/sidebar/_profile.html.haml
index 2061eac917f..7dd33f3c641 100644
--- a/app/views/layouts/nav/sidebar/_profile.html.haml
+++ b/app/views/layouts/nav/sidebar/_profile.html.haml
@@ -28,6 +28,8 @@
= link_to profile_account_path do
%strong.fly-out-top-item-name
= _('Account')
+
+ = render_if_exists 'layouts/nav/sidebar/profile_billing_link'
= nav_link(controller: 'oauth/applications') do
= link_to applications_profile_path do
.nav-icon-container
@@ -151,4 +153,6 @@
%strong.fly-out-top-item-name
= _('Authentication Log')
+ = render_if_exists 'layouts/nav/sidebar/profile_pipeline_quota_link'
+
= render 'shared/sidebar_toggle_button'
diff --git a/app/views/layouts/nav/sidebar/_project.html.haml b/app/views/layouts/nav/sidebar/_project.html.haml
index 3a0c2b9c284..399305baec1 100644
--- a/app/views/layouts/nav/sidebar/_project.html.haml
+++ b/app/views/layouts/nav/sidebar/_project.html.haml
@@ -270,6 +270,8 @@
%span= _("Got it!")
= sprite_icon('thumb-up')
+ = render_if_exists 'layouts/nav/sidebar/project_feature_flags_link'
+
- if project_nav_tab? :container_registry
= nav_link(controller: %w[projects/registry/repositories]) do
= link_to project_container_registry_index_path(@project), class: 'shortcuts-container-registry' do
@@ -283,7 +285,9 @@
%strong.fly-out-top-item-name
= _('Registry')
- - if project_nav_tab?(:wiki)
+ = render_if_exists 'layouts/nav/sidebar/project_packages_link'
+
+ - if project_nav_tab? :wiki
- wiki_url = project_wiki_path(@project, :home)
= nav_link(controller: :wikis) do
= link_to wiki_url, class: 'shortcuts-wiki qa-wiki-link' do
diff --git a/app/views/layouts/notify.html.haml b/app/views/layouts/notify.html.haml
index 8dff12c1b7f..de487a94d40 100644
--- a/app/views/layouts/notify.html.haml
+++ b/app/views/layouts/notify.html.haml
@@ -31,4 +31,6 @@
adjust your notification settings.
= email_action @target_url
+
+ = render_if_exists 'layouts/email_additional_text'
= html_footer_message
diff --git a/app/views/layouts/notify.text.erb b/app/views/layouts/notify.text.erb
index 248916fba63..0ee30c2a6cf 100644
--- a/app/views/layouts/notify.text.erb
+++ b/app/views/layouts/notify.text.erb
@@ -12,5 +12,6 @@
<% end -%>
<%= "You're receiving this email because #{notification_reason_text(@reason)}." %>
+<%= render_if_exists 'layouts/mailer/additional_text' %>
<%= text_footer_message -%>
diff --git a/app/views/notify/closed_issue_email.html.haml b/app/views/notify/closed_issue_email.html.haml
index eb148d72da1..f21cf1ad34b 100644
--- a/app/views/notify/closed_issue_email.html.haml
+++ b/app/views/notify/closed_issue_email.html.haml
@@ -1,2 +1,2 @@
%p
- Issue was closed by #{sanitize_name(@updated_by.name)}
+ Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
diff --git a/app/views/notify/closed_issue_email.text.haml b/app/views/notify/closed_issue_email.text.haml
index b1f0a3f37ec..5567adc9165 100644
--- a/app/views/notify/closed_issue_email.text.haml
+++ b/app/views/notify/closed_issue_email.text.haml
@@ -1,3 +1,3 @@
-Issue was closed by #{sanitize_name(@updated_by.name)}
+Issue was closed by #{sanitize_name(@updated_by.name)} #{closure_reason_text(@closed_via, format: formats.first)}.
Issue ##{@issue.iid}: #{project_issue_url(@issue.project, @issue)}
diff --git a/app/views/projects/branches/_branch.html.haml b/app/views/projects/branches/_branch.html.haml
index 91c51d5e091..0e15f581ddc 100644
--- a/app/views/projects/branches/_branch.html.haml
+++ b/app/views/projects/branches/_branch.html.haml
@@ -10,7 +10,7 @@
.branch-info
.branch-title
= sprite_icon('fork', size: 12)
- = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8' do
+ = link_to project_tree_path(@project, branch.name), class: 'item-title str-truncated-100 ref-name prepend-left-8 qa-branch-name' do
= branch.name
- if branch.name == @repository.root_ref
%span.badge.badge-primary.prepend-left-5 default
diff --git a/app/views/projects/branches/index.html.haml b/app/views/projects/branches/index.html.haml
index 43f1cd01b67..d270e461ac8 100644
--- a/app/views/projects/branches/index.html.haml
+++ b/app/views/projects/branches/index.html.haml
@@ -1,5 +1,6 @@
- @no_container = true
- page_title _('Branches')
+- add_to_breadcrumbs(_('Repository'), project_tree_path(@project))
%div{ class: container_class }
.top-area.adjust
@@ -44,6 +45,8 @@
= link_to new_project_branch_path(@project), class: 'btn btn-success' do
= s_('Branches|New branch')
+ = render_if_exists 'projects/commits/mirror_status'
+
- if can?(current_user, :admin_project, @project)
- project_settings_link = link_to s_('Branches|project settings'), project_protected_branches_path(@project)
.row-content-block
diff --git a/app/views/projects/project_members/_new_project_member.html.haml b/app/views/projects/project_members/_new_project_member.html.haml
index 0590578c3fe..efabb7f7b19 100644
--- a/app/views/projects/project_members/_new_project_member.html.haml
+++ b/app/views/projects/project_members/_new_project_member.html.haml
@@ -19,4 +19,5 @@
= text_field_tag :expires_at, nil, class: 'form-control js-access-expiration-date', placeholder: 'Expiration date'
%i.clear-icon.js-clear-input
= f.submit _("Add to project"), class: "btn btn-success qa-add-member-button"
- = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
+ - if can_import_members?
+ = link_to _("Import"), import_project_project_members_path(@project), class: "btn btn-default", title: _("Import members from another project")
diff --git a/app/views/shared/_clone_panel.html.haml b/app/views/shared/_clone_panel.html.haml
index a2df0347fd6..1e509ea0d1f 100644
--- a/app/views/shared/_clone_panel.html.haml
+++ b/app/views/shared/_clone_panel.html.haml
@@ -16,7 +16,12 @@
= ssh_clone_button(project)
%li
= http_clone_button(project)
+ = render_if_exists 'shared/kerberos_clone_button', project: project
= text_field_tag :project_clone, default_url_to_repo(project), class: "js-select-on-focus form-control", readonly: true, aria: { label: 'Project clone URL' }
.input-group-append
= clipboard_button(target: '#project_clone', title: _("Copy URL to clipboard"), class: "input-group-text btn-default btn-clipboard")
+
+ = render_if_exists 'shared/geo_modal_button'
+
+= render_if_exists 'shared/geo_modal', project: project
diff --git a/app/views/shared/members/_member.html.haml b/app/views/shared/members/_member.html.haml
index 2e5747121b6..2db1f67a793 100644
--- a/app/views/shared/members/_member.html.haml
+++ b/app/views/shared/members/_member.html.haml
@@ -53,7 +53,7 @@
= time_ago_with_tooltip(member.created_at)
- if show_roles
- current_resource = @project || @group
- .controls.member-controls.row
+ .controls.member-controls
- if show_controls && member.source == current_resource
- if member.can_resend_invite?
diff --git a/app/views/users/calendar_activities.html.haml b/app/views/users/calendar_activities.html.haml
index 01acbf8eadd..3191eaa1e2c 100644
--- a/app/views/users/calendar_activities.html.haml
+++ b/app/views/users/calendar_activities.html.haml
@@ -9,7 +9,7 @@
%i.fa.fa-clock-o
= event.created_at.to_time.in_time_zone.strftime('%-I:%M%P')
- if event.visible_to_user?(current_user)
- - if event.push?
+ - if event.push_action?
#{event.action_name} #{event.ref_type}
%strong
- commits_path = project_commits_path(event.project, event.ref_name)
diff --git a/app/workers/process_commit_worker.rb b/app/workers/process_commit_worker.rb
index 29a7f8e691a..3efb5343a96 100644
--- a/app/workers/process_commit_worker.rb
+++ b/app/workers/process_commit_worker.rb
@@ -48,7 +48,7 @@ class ProcessCommitWorker
# Issues::CloseService#execute.
IssueCollection.new(issues).updatable_by_user(user).each do |issue|
Issues::CloseService.new(project, author)
- .close_issue(issue, commit: commit)
+ .close_issue(issue, closed_via: commit)
end
end
diff --git a/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml
new file mode 100644
index 00000000000..bb2fc9af2a1
--- /dev/null
+++ b/changelogs/unreleased/19569-include-information-if-issue-was-closed-via-mr.yml
@@ -0,0 +1,5 @@
+---
+title: Include information if issue was clossed via merge request or commit
+merge_request: 15610
+author: Michał Zając
+type: changed
diff --git a/changelogs/unreleased/61550-next-badge.yml b/changelogs/unreleased/61550-next-badge.yml
new file mode 100644
index 00000000000..122e394a68c
--- /dev/null
+++ b/changelogs/unreleased/61550-next-badge.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes next badge being always visible
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/61606-support-string-piwik-website-ids.yml b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml
new file mode 100644
index 00000000000..5c525294132
--- /dev/null
+++ b/changelogs/unreleased/61606-support-string-piwik-website-ids.yml
@@ -0,0 +1,5 @@
+---
+title: "Supports Matomo/Piwik string website ID (\"Protect Track ID\" plugin)"
+merge_request: 28214
+author: DUVERGIER Claude
+type: fixed \ No newline at end of file
diff --git a/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml
new file mode 100644
index 00000000000..c2dcd309abd
--- /dev/null
+++ b/changelogs/unreleased/ce-11542-remove-non-semantic-use-of-row-in-member-listing-controls.yml
@@ -0,0 +1,5 @@
+---
+title: Remove non-semantic use of `.row` in member listing controls
+merge_request: 28204
+author:
+type: fixed
diff --git a/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml
new file mode 100644
index 00000000000..332105bb269
--- /dev/null
+++ b/changelogs/unreleased/ce-quick-fix-58727-collapsed-sidebar-flyout-menu-items-don-t-appear-in-1200px-screen-size.yml
@@ -0,0 +1,5 @@
+---
+title: Fix flyout nav on small viewports
+merge_request: 25998
+author:
+type: fixed
diff --git a/changelogs/unreleased/diff-whitespace-setting-changes.yml b/changelogs/unreleased/diff-whitespace-setting-changes.yml
new file mode 100644
index 00000000000..640e9e589df
--- /dev/null
+++ b/changelogs/unreleased/diff-whitespace-setting-changes.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed show whitespace button not refetching diff content
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/fix-project-visibility-level-validation.yml b/changelogs/unreleased/fix-project-visibility-level-validation.yml
new file mode 100644
index 00000000000..9581a475842
--- /dev/null
+++ b/changelogs/unreleased/fix-project-visibility-level-validation.yml
@@ -0,0 +1,5 @@
+---
+title: Fix project visibility level validation
+merge_request: 28305
+author: Peter Marko
+type: fixed
diff --git a/changelogs/unreleased/include-ee-fixtures.yml b/changelogs/unreleased/include-ee-fixtures.yml
new file mode 100644
index 00000000000..ba500d92de3
--- /dev/null
+++ b/changelogs/unreleased/include-ee-fixtures.yml
@@ -0,0 +1,5 @@
+---
+title: Add EE fixtures to SeedFu list
+merge_request: 28241
+author:
+type: other
diff --git a/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml
new file mode 100644
index 00000000000..daeefd3ffd7
--- /dev/null
+++ b/changelogs/unreleased/make-autocomplete-faster-with-lots-of-results.yml
@@ -0,0 +1,5 @@
+---
+title: Improve performance of users autocomplete when there are lots of results
+merge_request:
+author:
+type: performance
diff --git a/changelogs/unreleased/sh-fix-rebase-error-clearing.yml b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml
new file mode 100644
index 00000000000..4f5f2779e7f
--- /dev/null
+++ b/changelogs/unreleased/sh-fix-rebase-error-clearing.yml
@@ -0,0 +1,5 @@
+---
+title: Properly clear the merge error upon rebase failure
+merge_request: 28319
+author:
+type: fixed
diff --git a/changelogs/unreleased/sh-update-process-mem.yml b/changelogs/unreleased/sh-update-process-mem.yml
new file mode 100644
index 00000000000..51b22fb0f00
--- /dev/null
+++ b/changelogs/unreleased/sh-update-process-mem.yml
@@ -0,0 +1,5 @@
+---
+title: Update get_process_mem to 0.2.3
+merge_request: 28248
+author:
+type: other
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index bff809b7661..23377b43f78 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -245,7 +245,7 @@ production: &base
host: example.com
port: 80 # Set to 443 if you serve the pages with HTTPS
https: false # Set to true if you serve the pages with HTTPS
- artifacts_server: true
+ artifacts_server: true # Set to false if you want to disable online view of HTML artifacts
# external_http: ["1.1.1.1:80", "[2001::1]:80"] # If defined, enables custom domain support in GitLab Pages
# external_https: ["1.1.1.1:443", "[2001::1]:443"] # If defined, enables custom domain and certificate support in GitLab Pages
admin:
diff --git a/config/initializers/01_secret_token.rb b/config/initializers/01_secret_token.rb
index 4328ca509ba..e24b5cbd510 100644
--- a/config/initializers/01_secret_token.rb
+++ b/config/initializers/01_secret_token.rb
@@ -1,3 +1,14 @@
+# WARNING: If you add a new secret to this file, make sure you also
+# update Omnibus GitLab or updates will fail. Omnibus is responsible for
+# writing the `secrets.yml` file. If Omnibus doesn't know about a
+# secret, Rails will attempt to write to the file, but this will fail
+# because Rails doesn't have write access.
+#
+# As an example:
+# * https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27581
+# * https://gitlab.com/gitlab-org/omnibus-gitlab/merge_requests/3267
+#
+#
# This file needs to be loaded BEFORE any initializers that attempt to
# prepend modules that require access to secrets (e.g. EE's 0_as_concern.rb).
#
diff --git a/config/initializers/seed_fu.rb b/config/initializers/seed_fu.rb
new file mode 100644
index 00000000000..2e48e41a311
--- /dev/null
+++ b/config/initializers/seed_fu.rb
@@ -0,0 +1,4 @@
+# frozen_string_literal: true
+if Gitlab.ee?
+ SeedFu.fixture_paths += %W[ee/db/fixtures ee/db/fixtures/#{Rails.env}]
+end
diff --git a/config/karma.config.js b/config/karma.config.js
index 83ba46345f2..b2fc3a32816 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -107,6 +107,8 @@ module.exports = function(config) {
// chrome cannot run in sandboxed mode inside a docker container unless it is run with
// escalated kernel privileges (e.g. docker run --cap-add=CAP_SYS_ADMIN)
'--no-sandbox',
+ // https://bugs.chromium.org/p/chromedriver/issues/detail?id=2870
+ '--enable-features=NetworkService,NetworkServiceInProcess',
],
},
},
diff --git a/config/routes/admin.rb b/config/routes/admin.rb
index 90d7f4a04d4..bc19219a0b8 100644
--- a/config/routes/admin.rb
+++ b/config/routes/admin.rb
@@ -111,6 +111,7 @@ namespace :admin do
put :reset_health_check_token
put :clear_repository_check_states
get :integrations, :repository, :templates, :ci_cd, :reporting, :metrics_and_profiling, :network, :geo, :preferences
+ get :lets_encrypt_terms_of_service
end
resources :labels
diff --git a/doc/administration/geo/disaster_recovery/background_verification.md b/doc/administration/geo/disaster_recovery/background_verification.md
index 7d2fd51f834..c7299b6e196 100644
--- a/doc/administration/geo/disaster_recovery/background_verification.md
+++ b/doc/administration/geo/disaster_recovery/background_verification.md
@@ -157,6 +157,42 @@ For wikis:
sudo -u git -H bundle exec rake geo:verification:wiki:reset RAILS_ENV=production
```
+## Reconcile differences with checksum mismatches
+
+If the **primary** and **secondary** nodes have a checksum verification mismatch, the cause may not be apparent. To find the cause of a checksum mismatch:
+
+1. Navigate to the **Admin Area > Projects** dashboard on the **primary** node, find the
+ project that you want to check the checksum differences and click on the
+ **Edit** button:
+ ![Projects dashboard](img/checksum-differences-admin-projects.png)
+
+1. On the project admin page get the **Gitaly storage name**, and **Gitaly relative path**:
+ ![Project admin page](img/checksum-differences-admin-project-page.png)
+
+1. Navigate to the project's repository directory on both **primary** and **secondary** nodes. For an installation from source, the path is usually `/home/git/repositories`. For Omnibus installs, the path is usually `/var/opt/gitlab/git-data/repositories`. Note that if `git_data_dirs` is customized, check the directory layout on your server to be sure.
+
+ ```sh
+ cd /var/opt/gitlab/git-data/repositories
+ ```
+
+1. Run the following command on the **primary** node, redirecting the output to a file:
+
+ ```sh
+ git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > primary-node-refs
+ ```
+
+1. Run the following command on the **secondary** node, redirecting the output to a file:
+
+ ```sh
+ git show-ref --head | grep -E "HEAD|(refs/(heads|tags|keep-around|merge-requests|environments|notes)/)" > secondary-node-refs
+ ```
+
+1. Copy the files from the previous steps on the same system, and do a diff between the contents:
+
+ ```sh
+ diff primary-node-refs secondary-node-refs
+ ```
+
## Current limitations
Until [issue #5064][ee-5064] is completed, background verification doesn't cover
diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png
new file mode 100644
index 00000000000..fd51523104b
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-project-page.png
Binary files differ
diff --git a/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png
new file mode 100644
index 00000000000..b2a6da69d3d
--- /dev/null
+++ b/doc/administration/geo/disaster_recovery/img/checksum-differences-admin-projects.png
Binary files differ
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index d0e0e320019..caadec3ac4e 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -41,7 +41,6 @@ options:
NOTE: **Note:**
This is only available starting in certain versions of GitLab:
-
* 11.5.11
* 11.6.11
* 11.7.12
@@ -59,13 +58,13 @@ details.
To do this, run the Rake task:
```sh
-gitlab-rake gitlab:features:enable_rugged
+sudo gitlab-rake gitlab:features:enable_rugged
```
If you need to undo this setting for some reason, run:
```sh
-gitlab-rake gitlab:features:disable_rugged
+sudo gitlab-rake gitlab:features:disable_rugged
```
### Known issues
diff --git a/doc/api/repository_files.md b/doc/api/repository_files.md
index 6fcc06ea8cd..87c7f371de1 100644
--- a/doc/api/repository_files.md
+++ b/doc/api/repository_files.md
@@ -181,7 +181,7 @@ Currently gitlab-shell has a boolean return code, preventing GitLab from specify
## Delete existing file in repository
-This allows you to delete a single file. For deleting multiple files with a singleh request see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions).
+This allows you to delete a single file. For deleting multiple files with a single request, see the [commits API](commits.html#create-a-commit-with-multiple-files-and-actions).
```
DELETE /projects/:id/repository/files/:file_path
diff --git a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
index 99a4316ab0d..47d20a4e1c1 100644
--- a/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
+++ b/doc/ci/examples/test-and-deploy-python-application-to-heroku.md
@@ -9,7 +9,12 @@ You can checkout the [example source](https://gitlab.com/ayufan/python-getting-s
This is what the `.gitlab-ci.yml` file looks like for this project:
```yaml
+stages:
+ - test
+ - deploy
+
test:
+ stage: test
script:
# this configures Django application to use attached postgres database that is run on `postgres` host
- export DATABASE_URL=postgres://postgres:@postgres:5432/python-test-app
@@ -19,7 +24,7 @@ test:
- python manage.py test
staging:
- type: deploy
+ stage: deploy
script:
- apt-get update -qy
- apt-get install -y ruby-dev
@@ -29,7 +34,7 @@ staging:
- master
production:
- type: deploy
+ stage: deploy
script:
- apt-get update -qy
- apt-get install -y ruby-dev
diff --git a/doc/ci/introduction/index.md b/doc/ci/introduction/index.md
index 14ea648c00b..0045fa2fb1f 100644
--- a/doc/ci/introduction/index.md
+++ b/doc/ci/introduction/index.md
@@ -156,7 +156,7 @@ Once you're happy with your implementation:
![GitLab workflow example](img/gitlab_workflow_example_11_9.png)
-GitLab CI/CD is capable of a doing a lot more, but this workflow
+GitLab CI/CD is capable of doing a lot more, but this workflow
exemplifies GitLab's ability to track the entire process,
without the need of any external tool to deliver your software.
And, most usefully, you can visualize all the steps through
diff --git a/doc/ci/services/mysql.md b/doc/ci/services/mysql.md
index 2902c30c7c0..0de6a93514a 100644
--- a/doc/ci/services/mysql.md
+++ b/doc/ci/services/mysql.md
@@ -17,8 +17,8 @@ services:
variables:
# Configure mysql environment variables (https://hub.docker.com/_/mysql/)
- MYSQL_DATABASE: el_duderino
- MYSQL_ROOT_PASSWORD: mysql_strong_password
+ MYSQL_DATABASE: "<your_mysql_database>"
+ MYSQL_ROOT_PASSWORD: "mysql_strong_password"
```
And then configure your application to use the database, for example:
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index dca2d953286..36b88ff12c2 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -337,6 +337,7 @@ In addition, `only` and `except` allow the use of special keywords:
| `triggers` | For pipelines created using a trigger token. |
| `web` | For pipelines created using **Run pipeline** button in GitLab UI (under your project's **Pipelines**). |
| `merge_requests` | When a merge request is created or updated (See [pipelines for merge requests](../merge_request_pipelines/index.md)). |
+| `chats` | For jobs created using a [GitLab ChatOps](../chatops/README.md) command. |
In the example below, `job` will run only for refs that start with `issue-`,
whereas all branches will be skipped:
@@ -1446,7 +1447,7 @@ be automatically shown in merge requests.
> Introduced in GitLab 11.5. Requires GitLab Runner 11.5 and above.
-The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+The `sast` report collects [SAST vulnerabilities](https://docs.gitlab.com/ee/user/application_security/sast/index.html)
as artifacts.
The collected SAST report will be uploaded to GitLab as an artifact and will
@@ -1764,9 +1765,6 @@ TIP: **Tip:**
Use merging to customize and override included CI/CD configurations with local
definitions.
-Recursive includes are not supported. Your external files should not use the
-`include` keyword as it will be ignored.
-
NOTE: **Note:**
Using YAML aliases across different YAML files sourced by `include` is not
supported. You must only refer to aliases in the same file. Instead
diff --git a/doc/development/README.md b/doc/development/README.md
index 83a1145c020..cfbbd5aaeaa 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -44,6 +44,7 @@ description: 'Learn how to contribute to GitLab.'
- [`Gemfile` guidelines](gemfile.md)
- [Pry debugging](pry_debugging.md)
- [Sidekiq debugging](sidekiq_debugging.md)
+- [Accessing session data](session.md)
- [Gotchas](gotchas.md) to avoid
- [Avoid modules with instance variables](module_with_instance_variables.md) if possible
- [How to dump production data to staging](db_dump.md)
diff --git a/doc/development/architecture.md b/doc/development/architecture.md
index 88c16a8db22..13de1f55f23 100644
--- a/doc/development/architecture.md
+++ b/doc/development/architecture.md
@@ -16,9 +16,18 @@ This document is designed to be consumed by systems adminstrators and GitLab Sup
When deployed, GitLab should be considered the amalgamation of the below processes. When troubleshooting or debugging, be as specific as possible as to which component you are referencing. That should increase clarity and reduce confusion.
+### Layers
+
+GitLab can be considered to have two layers from a process perspective:
+
+- **Monitoring**: Anything from this layer is not required to deliver GitLab the application, but will allow administrators more insight into their infrastructure and what the service as a whole is doing.
+- **Core**: Any process that is vital for the delivery of GitLab as a platform. If any of these processes halt there will be a GitLab outage. For the Core layer, you can further divide into:
+ - **Processors**: These processes are responsible for actually performing operations and presenting the service.
+ - **Data**: These services store/expose structured data for the GitLab service.
+
### GitLab Process Descriptions
-As of this writing, a fresh GitLab 11.3.0 install will show the following processes with `gitlab-ctl status`:
+As of this writing, a fresh GitLab 11.3.0 install with default settings will show the following processes with `gitlab-ctl status`:
```
run: alertmanager: (pid 30829) 14207s; run: log: (pid 13906) 2432044s
@@ -37,86 +46,77 @@ run: sidekiq: (pid 30953) 14205s; run: log: (pid 13810) 2432047s
run: unicorn: (pid 30960) 14204s; run: log: (pid 13809) 2432047s
```
-### Layers
-
-GitLab can be considered to have two layers from a process perspective:
-
-- **Monitoring**: Anything from this layer is not required to deliver GitLab the application, but will allow administrators more insight into their infrastructure and what the service as a whole is doing.
-- **Core**: Any process that is vital for the delivery of GitLab as a platform. If any of these processes halt there will be a GitLab outage. For the Core layer, you can further divide into:
- - **Processors**: These processes are responsible for actually performing operations and presenting the service.
- - **Data**: These services store/expose structured data for the GitLab service.
-
-### alertmanager
+#### alertmanager
- Omnibus configuration options
- Layer: Monitoring
-[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on.
+[Alert manager](https://prometheus.io/docs/alerting/alertmanager/) is a tool provided by Prometheus that _"handles alerts sent by client applications such as the Prometheus server. It takes care of deduplicating, grouping, and routing them to the correct receiver integration such as email, PagerDuty, or OpsGenie. It also takes care of silencing and inhibition of alerts."_ You can read more in [issue gitlab-ce#45740](https://gitlab.com/gitlab-org/gitlab-ce/issues/45740) about what we will be alerting on.
-### gitaly
+#### gitaly
- [Omnibus configuration options](https://gitlab.com/gitlab-org/gitaly/tree/master/doc/configuration)
- Layer: Core Service (Data)
-Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (Think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
+Gitaly is a service designed by GitLab to remove our need for NFS for Git storage in distributed deployments of GitLab (think GitLab.com or High Availability Deployments). As of 11.3.0, this service handles all Git level access in GitLab. You can read more about the project [in the project's readme](https://gitlab.com/gitlab-org/gitaly).
-### gitlab-monitor
+#### gitlab-monitor
- Omnibus configuration options
- Layer: Monitoring
-GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor)
+GitLab Monitor is a process designed in house that allows us to export metrics about GitLab application internals to Prometheus. You can read more [in the project's readme](https://gitlab.com/gitlab-org/gitlab-monitor).
-### gitlab-workhorse
+#### gitlab-workhorse
- Omnibus configuration options
- Layer: Core Service (Processor)
-[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
+[GitLab Workhorse](https://gitlab.com/gitlab-org/gitlab-workhorse) is a program designed at GitLab to help alleviate pressure from Unicorn. You can read more about the [historical reasons for developing](https://about.gitlab.com/2016/04/12/a-brief-history-of-gitlab-workhorse/). It's designed to act as a smart reverse proxy to help speed up GitLab as a whole.
-### logrotate
+#### logrotate
- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/logs.html#logrotate)
- Layer: Core Service
-GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common opensource offering.
+GitLab is comprised of a large number of services that all log. We started bundling our own logrotate as of 7.4 to make sure we were logging responsibly. This is just a packaged version of the common open source offering.
-### nginx
+#### nginx
- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/nginx.html)
- Layer: Core Service (Processor)
Nginx as an ingress port for all HTTP requests and routes them to the approriate sub-systems within GitLab. We are bundling an unmodified version of the popular open source webserver.
-### node-exporter
+#### node-exporter
- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/node_exporter.html)
- Layer: Monitoring
-[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine. (Think CPU/Disk/Load) It's just a packaged version of the common open source offering from the Prometheus project.
+[Node Exporter](https://github.com/prometheus/node_exporter) is a Prometheus tool that gives us metrics on the underlying machine (think CPU/Disk/Load). It's just a packaged version of the common open source offering from the Prometheus project.
-### postgres-exporter
+#### postgres-exporter
- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/postgres_exporter.html)
- Layer: Monitoring
-[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to prometheus for use in Grafana Dashboards.
+[Postgres-exporter](https://github.com/wrouesnel/postgres_exporter) is the community provided Prometheus exporter that will deliver data about Postgres to Prometheus for use in Grafana Dashboards.
-### postgresql
+#### postgresql
- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/database.html)
- Layer: Core Service (Data)
GitLab packages the popular Database to provide storage for Application meta data and user information.
-### prometheus
+#### prometheus
- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/)
- Layer: Monitoring
Prometheus is a time-series tool that helps GitLab administrators expose metrics about the individual processes used to provide GitLab the service.
-### redis
+#### redis
- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/redis.html)
- Layer: Core Service (Data)
@@ -125,40 +125,42 @@ Redis is packaged to provide a place to store:
- session data
- temporary cache information
-- background job queues.
+- background job queues
-### redis-exporter
+#### redis-exporter
- [Omnibus configuration options](https://docs.gitlab.com/ee/administration/monitoring/prometheus/redis_exporter.html)
- Layer: Monitoring
-[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Graphana.
+[Redis Exporter](https://github.com/oliver006/redis_exporter) is designed to give specific metrics about the Redis process to Prometheus so that we can graph these metrics in Grafana.
-### sidekiq
+#### sidekiq
- Omnibus configuration options
- Layer: Core Service (Processor)
Sidekiq is a Ruby background job processor that pulls jobs from the redis queue and processes them. Background jobs allow GitLab to provide a faster request/response cycle by moving work into the background.
-### unicorn
+#### unicorn
- [Omnibus configuration options](https://docs.gitlab.com/omnibus/settings/unicorn.html)
- Layer: Core Service (Processor)
[Unicorn](https://bogomips.org/unicorn/) is a Ruby application server that is used to run the core Rails Application that provides the user facing features in GitLab. Often process output you will see this as `bundle` or `config.ru` depending on the GitLab version.
-### Additional Processes
+#### Additional Processes
+
+The following processes will be running if corresponding feature is enabled.
-### GitLab Pages
+##### gitlab-pages
TODO
-### Mattermost
+##### mattermost
TODO
-### Registry
+##### registry
The registry is what users use to store their own Docker images. The bundled
registry uses nginx as a load balancer and GitLab as an authentication manager.
@@ -181,10 +183,10 @@ It's important to understand the distinction as some processes are used in both
### GitLab Web HTTP Request Cycle
-When making a request to an HTTP Endpoint (Think `/users/sign_in`) the request will take the following path through the GitLab Service:
+When making a request to an HTTP Endpoint (think `/users/sign_in`) the request will take the following path through the GitLab Service:
-- nginx - Acts as our first line reverse proxy
-- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on unicorn.
+- nginx - Acts as our first line reverse proxy.
+- gitlab-workhorse - This determines if it needs to go to the Rails application or somewhere else to reduce load on Unicorn.
- unicorn - Since this is a web request, and it needs to access the application it will go to Unicorn.
- Postgres/Gitaly/Redis - Depending on the type of request, it may hit these services to store or retrieve data.
@@ -200,7 +202,7 @@ TODO
## System Layout
-When referring to `~git` in the pictures it means the home directory of the git user which is typically /home/git.
+When referring to `~git` in the pictures it means the home directory of the git user which is typically `/home/git`.
GitLab is primarily installed within the `/home/git` user home directory as `git` user. Within the home directory is where the gitlabhq server software resides as well as the repositories (though the repository location is configurable).
@@ -322,7 +324,7 @@ graph TB
| 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 |
+| 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.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index 52a4bb27817..5633c8313a3 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -52,6 +52,58 @@ Adhere to the [Documentation Style Guide](styleguide.md). If a style standard is
See the [Structure](styleguide.md#structure) section of the [Documentation Style Guide](styleguide.md).
+## Single codebase
+
+We currently maintain two sets of docs: one in the
+[gitlab-ce](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/doc) repo and
+one in [gitlab-ee](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/doc).
+They are similar, and most pages are identical, but they are different repositories.
+With the single codebase effort, we want to make those two sets identical, so when the
+time comes to have only one codebase, we'll be ready.
+
+Here are some links to get you up to speed with the current effort:
+
+- [CE/EE codebases blueprint](https://about.gitlab.com/handbook/engineering/infrastructure/blueprint/ce-ee-codebases/)
+- [CE/EE codebases merge design](https://about.gitlab.com/handbook/engineering/infrastructure/design/merge-ce-ee-codebases/)
+- [Single docs codebase epic](https://gitlab.com/groups/gitlab-org/-/epics/199)
+- [Issue board of related issues](https://gitlab.com/groups/gitlab-org/-/boards/981090?&label_name[]=Documentation&label_name[]=single%20codebase)
+- [Related merge requests](https://gitlab.com/groups/gitlab-org/-/merge_requests?scope=all&utf8=%E2%9C%93&state=all&label_name[]=Documentation&label_name[]=single%20codebase)
+- [Visualize the existing diffs](https://leipert-projects.gitlab.io/is-gitlab-pretty-yet/diff/?search=%5Edoc)
+
+### CE first
+
+After a given documentation path is aligned across CE and EE, all merge requests
+affecting that path must be submitted to CE, regardless of the content it has.
+This means that for EE-only features which are being added only to the EE codebase,
+you have to submit a separate merge request in CE that contains the docs.
+This might seem like a duplicate effort, but it's for the short term.
+A list of the already aligned docs can be found in
+[the epic description](https://gitlab.com/groups/gitlab-org/-/epics/199#ee-specific-lines-check).
+
+Since the docs will be combined, it's crucial to add the relevant
+[product badges](styleguide.md#product-badges) for all EE documentation, so that
+we can discern which features belong to which tier.
+
+### EE specific lines check
+
+There's a special test in place
+([`ee_specific_check.rb`](https://gitlab.com/gitlab-org/gitlab-ee/blob/master/scripts/ee_specific_check/ee_specific_check.rb)),
+which, among others, checks and prevents creating/editing new files and directories
+in EE under `doc/`.
+
+We have a long list of documentation paths that are either whitelisted or not.
+Paths in the whitelist (not commented out) will not be subject to the test,
+which means you are allowed to create/change docs content in EE for the time
+being. The goal is to not have any doc whitelisted.
+
+At the time of this writing, the only items left to be aligned are:
+
+- `doc/api/*` ([issue](https://gitlab.com/gitlab-org/gitlab-ce/issues/60045) / [merge request](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/27491))
+- `doc/README.md`
+
+Eventually, once all docs are aligned, we'll remove any doc reference from that
+script, so it catches everything.
+
## Changing document location
Changing a document's location requires specific steps to be followed to ensure that
@@ -438,6 +490,10 @@ Currently, the following tests are in place:
As CE is merged into EE once a day, it's important to avoid merge conflicts.
Submitting an EE-equivalent merge request cherry-picking all commits from CE to EE is
essential to avoid them.
+1. [`ee-files-location-check`/`ee-specific-lines-check`](#ee-specific-lines-check) (runs on EE only):
+ This test ensures that no new files/directories are created/changed in EE.
+ All docs should be submitted in CE instead, regardless the tier they are on.
+ This is for the [single codebase](#single-codebase) effort.
1. In a full pipeline, tests for [`/help`](#gitlab-help-tests).
### Linting
diff --git a/doc/development/fe_guide/graphql.md b/doc/development/fe_guide/graphql.md
index 5f6123b5f9b..8e06aa5d173 100644
--- a/doc/development/fe_guide/graphql.md
+++ b/doc/development/fe_guide/graphql.md
@@ -43,9 +43,9 @@ new Vue({
Read more about [Vue Apollo][vue-apollo] in the [Vue Apollo documentation][vue-apollo-docs].
-### Local state with `apollo-link-state`
+### Local state with Apollo
-It is possible to use our Apollo setup with [apollo-link-state][apollo-link-state] by passing
+It is possible to manage an application state with Apollo by passing
in a resolvers object when creating the default client. The default state can be set by writing
to the cache after setting up the default client.
@@ -76,6 +76,8 @@ const apolloProvider = new VueApollo({
});
```
+Read more about local state management with Apollo in the [Vue Apollo documentation](https://vue-apollo.netlify.com/guide/local-state.html#local-state).
+
### Testing
With [Vue test utils][vue-test-utils] it is easy to quickly test components that
@@ -92,6 +94,8 @@ it('tests apollo component', () => {
});
```
+Another possible way is testing queries with mocked GraphQL schema. Read more about this way in [Vue Apollo testing documentation](https://vue-apollo.netlify.com/guide/testing.html#tests-with-mocked-graqhql-schema)
+
## Usage outside of Vue
It is also possible to use GraphQL outside of Vue by directly importing
diff --git a/doc/development/fe_guide/vuex.md b/doc/development/fe_guide/vuex.md
index 7d52cac5f7e..bf248b7f8af 100644
--- a/doc/development/fe_guide/vuex.md
+++ b/doc/development/fe_guide/vuex.md
@@ -186,7 +186,7 @@ Remember that actions only describe that something happened, they don't describe
state.users.push(user);
},
[types.REQUEST_ADD_USER_ERROR](state, error) {
- state.isAddingUser = true;
+ state.isAddingUser = false;
state.errorAddingUser = error;
},
};
@@ -231,7 +231,7 @@ The store should be included in the main component of your application:
```javascript
// app.vue
- import store from 'store'; // it will include the index.js file
+ import store from './store'; // it will include the index.js file
export default {
name: 'application',
diff --git a/doc/development/go_guide/index.md b/doc/development/go_guide/index.md
index b9dc3797e5b..cf78b792ffd 100644
--- a/doc/development/go_guide/index.md
+++ b/doc/development/go_guide/index.md
@@ -40,7 +40,7 @@ of possible security breaches in our code:
- SQL injections
Remember to run
-[SAST](https://docs.gitlab.com/ee/user/project/merge_requests/sast.html)
+[SAST](https://docs.gitlab.com/ee/user/application_security/sast/index)
**[ULTIMATE]** on your project (or at least the [gosec
analyzer](https://gitlab.com/gitlab-org/security-products/analyzers/gosec)),
and to follow our [Security
@@ -96,7 +96,7 @@ dependency should be argued in the merge request, as per our [Approval
Guidelines](../code_review.md#approval-guidelines). Both [License
Management](https://docs.gitlab.com/ee/user/project/merge_requests/license_management.html)
**[ULTIMATE]** and [Dependency
-Scanning](https://docs.gitlab.com/ee/user/project/merge_requests/dependency_scanning.html)
+Scanning](https://docs.gitlab.com/ee/user/application_security/dependency_scanning/index)
**[ULTIMATE]** should be activated on all projects to ensure new dependencies
security status and license compatibility.
diff --git a/doc/development/session.md b/doc/development/session.md
new file mode 100644
index 00000000000..9edce3dbda0
--- /dev/null
+++ b/doc/development/session.md
@@ -0,0 +1,65 @@
+# Accessing session data
+
+Session data in GitLab is stored in Redis and can be accessed in a variety of ways.
+
+During a web request, for example:
+
+- Rails provides access to the session from within controllers through [`ActionDispatch::Session`](https://guides.rubyonrails.org/action_controller_overview.html#session).
+- Outside of controllers, it is possible to access the session through `Gitlab::Session`.
+
+Outside of a web request it is still possible to access sessions stored in Redis. For example:
+
+- Session IDs and contents can be [looked up directly in Redis](#redis).
+- Data about the UserAgent associated with the session can be accessed through `ActiveSession`.
+
+When storing values in a session it is best to:
+
+- Use simple primitives and avoid storing objects to avoid marshaling complications.
+- Clean up after unneeded variables to keep memory usage in Redis down.
+
+## Gitlab::Session
+
+Sometimes you might want to persist data in the session instead of another store like the database. `Gitlab::Session` lets you access this without passing the session around extensively. For example, you could access it from within a policy without having to pass the session through to each place permissions are checked from.
+
+The session has a hash-like interface, just like when using it from a controller. There is also `NamespacedSessionStore` for storing key-value data in a hash.
+
+```ruby
+# Lookup a value stored in the current session
+Gitlab::Session.current[:my_feature]
+
+# Modify the current session stored in redis
+Gitlab::Session.current[:my_feature] = value
+
+# Store key-value data namespaced under a key
+Gitlab::NamespacedSessionStore.new(:my_feature)[some_key] = value
+
+# Set the session for a block of code, such as for tests
+Gitlab::Session.with_session(my_feature: value) do
+ # Code that uses Session.current[:my_feature]
+end
+```
+
+## Redis
+
+Session data can be accessed directly through Redis. This can let you check up on a browser session when debugging.
+
+```ruby
+# Get a list of sessions
+session_ids = Gitlab::Redis::SharedState.with do |redis|
+ redis.smembers("#{Gitlab::Redis::SharedState::USER_SESSIONS_LOOKUP_NAMESPACE}:#{user.id}")
+end
+
+# Retrieve a specific session
+session_data = Gitlab::Redis::SharedState.with { |redis| redis.get("#{Gitlab::Redis::SharedState::SESSION_NAMESPACE}:#{session_id}") }
+Marshal.load(session_data)
+```
+
+## Getting device information with ActiveSession
+
+The [**Active Sessions** page on a user's profile](../user/profile/active_sessions.md) displays information about the device used to access each session. The methods used there to list sessions can also be useful for development.
+
+```ruby
+# Get list of sessions for a given user
+# Includes session_id and data from the UserAgent
+ActiveSession.list(user)
+```
diff --git a/doc/development/testing_guide/frontend_testing.md b/doc/development/testing_guide/frontend_testing.md
index f58a8dcbcdc..9bd99e80357 100644
--- a/doc/development/testing_guide/frontend_testing.md
+++ b/doc/development/testing_guide/frontend_testing.md
@@ -187,6 +187,7 @@ export default function doSomething() {
visitUrl('/foo/bar');
}
```
+
```js
// my_module_spec.js
import doSomething from '~/my_module';
@@ -213,7 +214,187 @@ Further documentation on the babel rewire pluign API can be found on
#### Waiting in tests
-If you cannot avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout) in tests, please use the [Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html).
+Sometimes a test needs to wait for something to happen in the application before it continues.
+Avoid using [`setTimeout`](https://developer.mozilla.org/en-US/docs/Web/API/WindowOrWorkerGlobalScope/setTimeout)
+because it makes the reason for waiting unclear and if passed a time larger than zero it will slow down our test suite.
+Instead use one of the following approaches.
+
+##### Promises and Ajax calls
+
+Register handler functions to wait for the `Promise` to be resolved.
+
+```javascript
+const askTheServer = () => {
+ return axios
+ .get('/endpoint')
+ .then(response => {
+ // do something
+ })
+ .catch(error => {
+ // do something else
+ });
+};
+```
+
+**in Jest:**
+
+```javascript
+it('waits for an Ajax call', () => {
+ return askTheServer().then(() => {
+ expect(something).toBe('done');
+ });
+});
+```
+
+**in Karma:**
+
+```javascript
+it('waits for an Ajax call', done => {
+ askTheServer()
+ .then(() => {
+ expect(something).toBe('done');
+ })
+ .then(done)
+ .catch(done.fail);
+});
+```
+
+If you are not able to register handlers to the `Promise`—for example because it is executed in a synchronous Vue life
+cycle hook—you can flush all pending `Promise`s:
+
+**in Jest:**
+
+```javascript
+it('waits for an Ajax call', () => {
+ synchronousFunction();
+ jest.runAllTicks();
+
+ expect(something).toBe('done');
+});
+```
+
+**in Karma:**
+
+You are out of luck. The following only works sometimes and may lead to flaky failures:
+
+```javascript
+it('waits for an Ajax call', done => {
+ synchronousFunction();
+
+ // create a new Promise and hope that it resolves after the rest
+ Promise.resolve()
+ .then(() => {
+ expect(something).toBe('done');
+ })
+ .then(done)
+ .catch(done.fail);
+});
+```
+
+##### Vue rendering
+
+To wait until a Vue component is re-rendered, use either of the equivalent
+[`Vue.nextTick()`](https://vuejs.org/v2/api/#Vue-nextTick) or `vm.$nextTick()`.
+
+**in Jest:**
+
+```javascript
+it('renders something', () => {
+ wrapper.setProps({ value: 'new value' });
+
+ return wrapper.vm.$nextTick().then(() => {
+ expect(wrapper.text()).toBe('new value');
+ });
+});
+```
+
+**in Karma:**
+
+```javascript
+it('renders something', done => {
+ wrapper.setProps({ value: 'new value' });
+
+ wrapper.vm
+ .$nextTick()
+ .then(() => {
+ expect(wrapper.text()).toBe('new value');
+ })
+ .then(done)
+ .catch(done.fail);
+});
+```
+
+##### `setTimeout()` / `setInterval()` in application
+
+If the application itself is waiting for some time, mock await the waiting. In Jest this is already
+[done by default](https://gitlab.com/gitlab-org/gitlab-ce/blob/a2128edfee799e49a8732bfa235e2c5e14949c68/jest.config.js#L47)
+(see also [Jest Timer Mocks](https://jestjs.io/docs/en/timer-mocks)). In Karma you can use the
+[Jasmine mock clock](https://jasmine.github.io/api/2.9/Clock.html).
+
+```javascript
+const doSomethingLater = () => {
+ setTimeout(() => {
+ // do something
+ }, 4000);
+};
+```
+
+**in Jest:**
+
+```javascript
+it('does something', () => {
+ doSomethingLater();
+ jest.runAllTimers();
+
+ expect(something).toBe('done');
+});
+```
+
+**in Karma:**
+
+```javascript
+it('does something', () => {
+ jasmine.clock().install();
+
+ doSomethingLater();
+ jasmine.clock().tick(4000);
+
+ expect(something).toBe('done');
+ jasmine.clock().uninstall();
+});
+```
+
+##### Events
+
+If the application triggers an event that you need to wait for in your test, register an event handler which contains
+the assertions:
+
+```javascript
+it('waits for an event', done => {
+ eventHub.$once('someEvent', eventHandler);
+
+ someFunction();
+
+ function eventHandler() {
+ expect(something).toBe('done');
+ done();
+ }
+});
+```
+
+In Jest you can also use a `Promise` for this:
+
+```javascript
+it('waits for an event', () => {
+ const eventTriggered = new Promise(resolve => eventHub.$once('someEvent', resolve));
+
+ someFunction();
+
+ return eventTriggered.then(() => {
+ expect(something).toBe('done');
+ });
+});
+```
#### Migrating flaky Karma tests to Jest
diff --git a/doc/install/installation.md b/doc/install/installation.md
index f16bc04af34..1fff3e92280 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -448,6 +448,18 @@ sudo -u git cp config/database.yml.postgresql config/database.yml
# MySQL only:
sudo -u git cp config/database.yml.mysql config/database.yml
+# PostgreSQL only:
+# Remove host, username, and password lines from config/database.yml.
+# Once modified, the `production` settings will be as follows:
+#
+# production:
+# adapter: postgresql
+# encoding: unicode
+# database: gitlabhq_production
+# pool: 10
+#
+sudo -u git -H editor config/database.yml
+
# MySQL and remote PostgreSQL only:
# Update username/password in config/database.yml.
# You only need to adapt the production settings (first part).
@@ -565,6 +577,18 @@ sudo -u git -H editor config.toml
For more information about configuring Gitaly see
[doc/administration/gitaly](../administration/gitaly).
+### Start Gitaly
+
+Gitaly must be running for the next section.
+
+```sh
+gitlab_path=/home/git/gitlab
+gitaly_path=/home/git/gitaly
+
+sudo -u git -H $gitlab_path/bin/daemon_with_pidfile $gitlab_path/tmp/pids/gitaly.pid \
+ $gitaly_path/gitaly $gitaly_path/config.toml >> $gitlab_path/log/gitaly.log 2>&1 &
+```
+
### Initialize Database and Activate Advanced Features
```sh
@@ -640,6 +664,12 @@ sudo -u git -H yarn install --production --pure-lockfile
sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
```
+If `rake` fails with `JavaScript heap out of memory` error, try to run it with `NODE_OPTIONS` set as follows.
+
+```sh
+sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
+```
+
### Start Your GitLab Instance
```sh
diff --git a/doc/integration/elasticsearch.md b/doc/integration/elasticsearch.md
index 7cef664bc98..b03bb6d98e8 100644
--- a/doc/integration/elasticsearch.md
+++ b/doc/integration/elasticsearch.md
@@ -85,7 +85,7 @@ To build and install the indexer, run:
```sh
git clone https://gitlab.com/gitlab-org/gitlab-elasticsearch-indexer.git
-cd /gitlab-elasticsearch-indexer
+cd gitlab-elasticsearch-indexer
make
sudo make install
```
diff --git a/doc/integration/salesforce.md b/doc/integration/salesforce.md
index 18d42486fd6..8a99641a256 100644
--- a/doc/integration/salesforce.md
+++ b/doc/integration/salesforce.md
@@ -1,15 +1,15 @@
# 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.
+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.
+To get the credentials (a pair of Client ID and Client Secret), you must register an application on SalesForces.
1. Sign in to [SalesForce](https://www.salesforce.com/).
-1. Navigate to **Platform Tools/Apps** and click on **New Connected App**.
+1. Navigate to **Platform Tools/Apps/App Manager** 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.
@@ -64,7 +64,7 @@ To get the credentials (a pair of Client ID and Client Secret), you must registe
}
```
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.
+1. Change `SALESFORCE_CLIENT_SECRET` to the Consumer Secret from the SalesForce connected application page.
![SalesForce App Secret Details](img/salesforce_app_secret_details.png)
1. Save the configuration file.
diff --git a/doc/update/patch_versions.md b/doc/update/patch_versions.md
index f90bff662e2..c2dff21b028 100644
--- a/doc/update/patch_versions.md
+++ b/doc/update/patch_versions.md
@@ -67,7 +67,7 @@ sudo -u git -H bundle exec rake gettext:pack RAILS_ENV=production
sudo -u git -H bundle exec rake gettext:po_to_json RAILS_ENV=production
# Clean up assets and cache
-sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile cache:clear RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
```
### 4. Update gitlab-workhorse to the corresponding version
diff --git a/doc/update/upgrading_from_source.md b/doc/update/upgrading_from_source.md
index 5118726cb0a..82a86f3f343 100644
--- a/doc/update/upgrading_from_source.md
+++ b/doc/update/upgrading_from_source.md
@@ -25,13 +25,7 @@ This section contains all the steps necessary to upgrade Community Edition or
Enterprise Edition, regardless of the version you are upgrading to. Version
specific guidelines (should there be any) are covered separately.
-### 1. Stop server
-
-```bash
-sudo service gitlab stop
-```
-
-### 2. Backup
+### 1. Backup
NOTE: If you installed GitLab from source, make sure `rsync` is installed.
@@ -41,6 +35,12 @@ cd /home/git/gitlab
sudo -u git -H bundle exec rake gitlab:backup:create RAILS_ENV=production
```
+### 2. Stop server
+
+```bash
+sudo service gitlab stop
+```
+
### 3. Update Ruby
NOTE: Beginning in GitLab 11.6, we only support Ruby 2.5 or higher, and dropped
@@ -314,7 +314,7 @@ sudo -u git -H bundle exec rake db:migrate RAILS_ENV=production
sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
# Update node dependencies and recompile assets
-sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+sudo -u git -H bundle exec rake yarn:install gitlab:assets:clean gitlab:assets:compile RAILS_ENV=production NODE_ENV=production NODE_OPTIONS="--max_old_space_size=4096"
# Clean up cache
sudo -u git -H bundle exec rake cache:clear RAILS_ENV=production
diff --git a/doc/user/application_security/dast/index.md b/doc/user/application_security/dast/index.md
index 904c9e8fefe..f3b7d7fd471 100644
--- a/doc/user/application_security/dast/index.md
+++ b/doc/user/application_security/dast/index.md
@@ -138,7 +138,7 @@ variables:
#### Customizing the DAST settings
-The SAST settings can be changed through environment variables by using the
+The DAST settings can be changed through environment variables by using the
[`variables`](../../../ci/yaml/README.md#variables) parameter in `.gitlab-ci.yml`.
These variables are documented in the [DAST README](https://gitlab.com/gitlab-org/security-products/dast#settings).
diff --git a/doc/user/group/custom_project_templates.md b/doc/user/group/custom_project_templates.md
index 8e101407ac0..f67325272a6 100644
--- a/doc/user/group/custom_project_templates.md
+++ b/doc/user/group/custom_project_templates.md
@@ -1,4 +1,4 @@
-# Custom group-level project templates **[PREMIUM ONLY]**
+# Custom group-level project templates **[PREMIUM]**
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ee/issues/6861) in [GitLab Premium](https://about.gitlab.com/pricing) 11.6.
diff --git a/doc/user/img/mermaid_diagram_render_gfm.png b/doc/user/img/mermaid_diagram_render_gfm.png
deleted file mode 100644
index 9d192a30a85..00000000000
--- a/doc/user/img/mermaid_diagram_render_gfm.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index c1c2c036bae..c4f9fe45211 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -480,7 +480,7 @@ GitLab 10.3.
> If this is not rendered correctly, see
https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#mermaid
-It is possible to generate diagrams and flowcharts from text using [Mermaid][mermaid].
+It is possible to generate diagrams and flowcharts from text using [Mermaid](https://mermaidjs.github.io/).
In order to generate a diagram or flowchart, you should write your text inside the `mermaid` block.
@@ -496,9 +496,15 @@ Example:
Becomes:
-<img src="./img/mermaid_diagram_render_gfm.png" width="200px" height="400px">
+```mermaid
+graph TD;
+ A-->B;
+ A-->C;
+ B-->D;
+ C-->D;
+```
-For details see the [Mermaid official page][mermaid].
+For details see the [Mermaid official page](https://mermaidjs.github.io/).
### Front matter
@@ -1072,7 +1078,6 @@ A link starting with a `/` is relative to the wiki root.
[^2]: This is my awesome footnote.
[markdown.md]: https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md
-[mermaid]: https://mermaidjs.github.io/ "Mermaid website"
[rouge]: http://rouge.jneen.net/ "Rouge website"
[redcarpet]: https://github.com/vmg/redcarpet "Redcarpet website"
[katex]: https://github.com/Khan/KaTeX "KaTeX website"
diff --git a/doc/user/permissions.md b/doc/user/permissions.md
index 9b298e4eb30..318053fdabb 100644
--- a/doc/user/permissions.md
+++ b/doc/user/permissions.md
@@ -11,7 +11,7 @@ project itself, the highest permission level is used.
On public and internal projects the Guest role is not enforced. All users will
be able to create issues, leave comments, and clone or download the project code.
-When a member leaves the team all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md)
+When a member leaves a team's project, all the assigned [Issues](project/issues/index.md) and [Merge Requests](project/merge_requests/index.md)
will be unassigned automatically.
GitLab [administrators](../administration/index.md) receive all permissions.
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 8e1603f9ec9..10eb3a4f3b7 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -321,8 +321,14 @@ X-Gitlab-Event: Issue Hook
"group_id": 41
}],
"changes": {
- "updated_by_id": [null, 1],
- "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "updated_by_id": {
+ "previous": null,
+ "current": 1
+ },
+ "updated_at": {
+ "previous": "2017-09-15 16:50:55 UTC",
+ "current": "2017-09-15 16:52:00 UTC"
+ },
"labels": {
"previous": [{
"id": 206,
@@ -851,8 +857,14 @@ X-Gitlab-Event: Merge Request Hook
"group_id": 41
}],
"changes": {
- "updated_by_id": [null, 1],
- "updated_at": ["2017-09-15 16:50:55 UTC", "2017-09-15 16:52:00 UTC"],
+ "updated_by_id": {
+ "previous": null,
+ "current": 1
+ },
+ "updated_at": {
+ "previous": "2017-09-15 16:50:55 UTC",
+ "current":"2017-09-15 16:52:00 UTC"
+ },
"labels": {
"previous": [{
"id": 206,
diff --git a/doc/user/project/new_ci_build_permissions_model.md b/doc/user/project/new_ci_build_permissions_model.md
index 6c3fa5eb463..d36312c9b8d 100644
--- a/doc/user/project/new_ci_build_permissions_model.md
+++ b/doc/user/project/new_ci_build_permissions_model.md
@@ -44,14 +44,14 @@ It is important to note that we have a few types of users:
- **Administrators**: CI jobs created by Administrators will not have access
to all GitLab projects, but only to projects and container images of projects
- that the administrator is a member of.That means that if a project is either
+ that the administrator is a member of. That means that if a project is either
public or internal users have access anyway, but if a project is private, the
Administrator will have to be a member of it in order to have access to it
via another project's job.
- **External users**: CI jobs created by [external users](../permissions.md#external-users-permissions) will have
access only to projects to which user has at least reporter access. This
- rules out accessing all internal projects by default,
+ rules out accessing all internal projects by default.
This allows us to make the CI and permission system more trustworthy.
Let's consider the following scenario:
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index 90ed24a2ded..296688ba25b 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -878,7 +878,7 @@ module API
expose :push_event_payload,
as: :push_data,
using: PushEventPayload,
- if: -> (event, _) { event.push? }
+ if: -> (event, _) { event.push_action? }
expose :author_username do |event, options|
event.author&.username
diff --git a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
index 9d99d04d263..876f53c66ba 100644
--- a/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
+++ b/lib/gitlab/ci/templates/Jobs/Deploy.gitlab-ci.yml
@@ -338,7 +338,6 @@ rollout 100%:
image_tag=${CI_APPLICATION_TAG:-$CI_COMMIT_TAG}
fi
- replicas="1"
service_enabled="true"
postgres_enabled="$POSTGRES_ENABLED"
diff --git a/lib/gitlab/lets_encrypt/challenge.rb b/lib/gitlab/lets_encrypt/challenge.rb
new file mode 100644
index 00000000000..6a7f5e965c5
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/challenge.rb
@@ -0,0 +1,17 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Challenge
+ def initialize(acme_challenge)
+ @acme_challenge = acme_challenge
+ end
+
+ delegate :url, :token, :file_content, :status, :request_validation, to: :acme_challenge
+
+ private
+
+ attr_reader :acme_challenge
+ end
+ end
+end
diff --git a/lib/gitlab/lets_encrypt/client.rb b/lib/gitlab/lets_encrypt/client.rb
new file mode 100644
index 00000000000..d7468b06767
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/client.rb
@@ -0,0 +1,74 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Client
+ PRODUCTION_DIRECTORY_URL = 'https://acme-v02.api.letsencrypt.org/directory'
+ STAGING_DIRECTORY_URL = 'https://acme-staging-v02.api.letsencrypt.org/directory'
+
+ def new_order(domain_name)
+ ensure_account
+
+ acme_order = acme_client.new_order(identifiers: [domain_name])
+
+ ::Gitlab::LetsEncrypt::Order.new(acme_order)
+ end
+
+ def load_order(url)
+ ensure_account
+
+ # rubocop: disable CodeReuse/ActiveRecord
+ ::Gitlab::LetsEncrypt::Order.new(acme_client.order(url: url))
+ # rubocop: enable CodeReuse/ActiveRecord
+ end
+
+ def load_challenge(url)
+ ensure_account
+
+ ::Gitlab::LetsEncrypt::Challenge.new(acme_client.challenge(url: url))
+ end
+
+ def terms_of_service_url
+ acme_client.terms_of_service
+ end
+
+ def enabled?
+ return false unless Feature.enabled?(:pages_auto_ssl)
+
+ Gitlab::CurrentSettings.lets_encrypt_terms_of_service_accepted
+ end
+
+ private
+
+ def acme_client
+ @acme_client ||= ::Acme::Client.new(private_key: private_key, directory: acme_api_directory_url)
+ end
+
+ def private_key
+ @private_key ||= OpenSSL::PKey.read(Gitlab::Application.secrets.lets_encrypt_private_key)
+ end
+
+ def admin_email
+ Gitlab::CurrentSettings.lets_encrypt_notification_email
+ end
+
+ def contact
+ "mailto:#{admin_email}"
+ end
+
+ def ensure_account
+ raise 'Acme integration is disabled' unless enabled?
+
+ @acme_account ||= acme_client.new_account(contact: contact, terms_of_service_agreed: true)
+ end
+
+ def acme_api_directory_url
+ if Rails.env.production?
+ PRODUCTION_DIRECTORY_URL
+ else
+ STAGING_DIRECTORY_URL
+ end
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lets_encrypt/order.rb b/lib/gitlab/lets_encrypt/order.rb
new file mode 100644
index 00000000000..5109b5e9843
--- /dev/null
+++ b/lib/gitlab/lets_encrypt/order.rb
@@ -0,0 +1,23 @@
+# frozen_string_literal: true
+
+module Gitlab
+ module LetsEncrypt
+ class Order
+ def initialize(acme_order)
+ @acme_order = acme_order
+ end
+
+ def new_challenge
+ authorization = @acme_order.authorizations.first
+ challenge = authorization.http
+ ::Gitlab::LetsEncrypt::Challenge.new(challenge)
+ end
+
+ delegate :url, :status, to: :acme_order
+
+ private
+
+ attr_reader :acme_order
+ end
+ end
+end
diff --git a/lib/gitlab/sentry.rb b/lib/gitlab/sentry.rb
index 356e6445e0e..72c44114001 100644
--- a/lib/gitlab/sentry.rb
+++ b/lib/gitlab/sentry.rb
@@ -10,7 +10,7 @@ module Gitlab
def self.context(current_user = nil)
return unless enabled?
- Raven.tags_context(locale: I18n.locale)
+ Raven.tags_context(default_tags)
if current_user
Raven.user_context(
@@ -44,16 +44,19 @@ module Gitlab
extra[:issue_url] = issue_url if issue_url
context # Make sure we've set everything we know in the context
- tags = {
- Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id
- }
-
- Raven.capture_exception(exception, tags: tags, extra: extra)
+ Raven.capture_exception(exception, tags: default_tags, extra: extra)
end
end
def self.should_raise_for_dev?
Rails.env.development? || Rails.env.test?
end
+
+ def self.default_tags
+ {
+ Labkit::Correlation::CorrelationId::LOG_KEY.to_sym => Labkit::Correlation::CorrelationId.current_id,
+ locale: I18n.locale
+ }
+ end
end
end
diff --git a/lib/tasks/gitlab/features.rake b/lib/tasks/gitlab/features.rake
index d115961108e..d88bcca0819 100644
--- a/lib/tasks/gitlab/features.rake
+++ b/lib/tasks/gitlab/features.rake
@@ -17,7 +17,7 @@ namespace :gitlab do
if status
Feature.enable(flag)
else
- Feature.disable(flag)
+ Feature.get(flag).remove
end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index 71ec8bcb9ba..68397c38700 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -868,6 +868,9 @@ msgstr ""
msgid "An error occurred while fetching the board lists. Please try again."
msgstr ""
+msgid "An error occurred while fetching the builds."
+msgstr ""
+
msgid "An error occurred while fetching the job log."
msgstr ""
@@ -883,6 +886,9 @@ msgstr ""
msgid "An error occurred while fetching the releases. Please try again."
msgstr ""
+msgid "An error occurred while fetching this tab."
+msgstr ""
+
msgid "An error occurred while getting projects"
msgstr ""
@@ -970,9 +976,15 @@ msgstr ""
msgid "Any"
msgstr ""
+msgid "Any Milestone"
+msgstr ""
+
msgid "Any encrypted tokens"
msgstr ""
+msgid "Any namespace"
+msgstr ""
+
msgid "Appearance"
msgstr ""
@@ -1060,6 +1072,12 @@ msgstr ""
msgid "Are you sure that you want to unarchive this project?"
msgstr ""
+msgid "Are you sure you want to cancel creating this comment?"
+msgstr ""
+
+msgid "Are you sure you want to cancel editing this comment?"
+msgstr ""
+
msgid "Are you sure you want to delete this list?"
msgstr ""
@@ -2578,6 +2596,9 @@ msgstr ""
msgid "Comment form position"
msgstr ""
+msgid "Comment is being updated"
+msgstr ""
+
msgid "Comments"
msgstr ""
@@ -3599,6 +3620,12 @@ msgstr ""
msgid "EmailError|Your account has been blocked. If you believe this is in error, contact a staff member."
msgstr ""
+msgid "EmailToken|reset it"
+msgstr ""
+
+msgid "EmailToken|resetting..."
+msgstr ""
+
msgid "Emails"
msgstr ""
@@ -3905,6 +3932,9 @@ msgstr ""
msgid "Error loading merge requests."
msgstr ""
+msgid "Error loading milestone tab"
+msgstr ""
+
msgid "Error loading project data. Please try again."
msgstr ""
@@ -4193,6 +4223,9 @@ msgstr ""
msgid "Failed to load errors from Sentry. Error message: %{errorMessage}"
msgstr ""
+msgid "Failed to load related branches"
+msgstr ""
+
msgid "Failed to promote label due to internal error. Please contact administrators."
msgstr ""
@@ -4872,7 +4905,7 @@ msgstr ""
msgid "I accept the|Terms of Service and Privacy Policy"
msgstr ""
-msgid "I have read and agree to the Let's Encrypt Terms of Service"
+msgid "I have read and agree to the Let's Encrypt %{link_start}Terms of Service%{link_end}"
msgstr ""
msgid "ID"
@@ -5217,6 +5250,9 @@ msgstr ""
msgid "Issue events"
msgstr ""
+msgid "Issue update failed"
+msgstr ""
+
msgid "IssueBoards|Board"
msgstr ""
@@ -6387,6 +6423,9 @@ msgstr ""
msgid "Note: Consider asking your GitLab administrator to configure %{github_integration_link}, which will allow login via GitHub and allow importing repositories without generating a Personal Access Token."
msgstr ""
+msgid "NoteForm|Note"
+msgstr ""
+
msgid "Notes|Are you sure you want to cancel creating this comment?"
msgstr ""
@@ -6402,6 +6441,9 @@ msgstr ""
msgid "Notes|Show history only"
msgstr ""
+msgid "Notes|This comment has changed since you started editing, please review the %{open_link}updated comment%{close_link} to ensure information is not lost"
+msgstr ""
+
msgid "Nothing to preview."
msgstr ""
@@ -10310,6 +10352,9 @@ msgstr ""
msgid "Unable to schedule a pipeline to run immediately"
msgstr ""
+msgid "Unable to update this issue at this time."
+msgstr ""
+
msgid "Unarchive project"
msgstr ""
@@ -11149,6 +11194,9 @@ msgstr ""
msgid "You need permission."
msgstr ""
+msgid "You need to be logged in."
+msgstr ""
+
msgid "You need to register a two-factor authentication app before you can set up a U2F device."
msgstr ""
@@ -11275,6 +11323,12 @@ msgstr ""
msgid "Your changes have been successfully committed."
msgstr ""
+msgid "Your comment could not be submitted! Please check your network connection and try again."
+msgstr ""
+
+msgid "Your comment could not be updated! Please check your network connection and try again."
+msgstr ""
+
msgid "Your comment will not be visible to the public."
msgstr ""
diff --git a/package.json b/package.json
index eb557101662..f19baac0229 100644
--- a/package.json
+++ b/package.json
@@ -1,13 +1,17 @@
{
"private": true,
"scripts": {
+ "check-dependencies": "yarn check --integrity",
"clean": "rm -rf public/assets tmp/cache/*-loader",
"dev-server": "NODE_OPTIONS=\"--max-old-space-size=3584\" nodemon -w 'config/webpack.config.js' --exec 'webpack-dev-server --config config/webpack.config.js'",
"eslint": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue .",
"eslint-fix": "eslint --max-warnings 0 --report-unused-disable-directives --ext .js,.vue --fix .",
"eslint-report": "eslint --max-warnings 0 --ext .js,.vue --format html --output-file ./eslint-report.html --no-inline-config .",
+ "prejest": "yarn check-dependencies",
+ "jest": "jest",
"jest-debug": "node --inspect-brk node_modules/.bin/jest --runInBand",
"jsdoc": "jsdoc -c config/jsdocs.config.js",
+ "prekarma": "yarn check-dependencies",
"karma": "BABEL_ENV=${BABEL_ENV:=karma} karma start --single-run true config/karma.config.js",
"karma-coverage": "BABEL_ENV=coverage karma start --single-run true config/karma.config.js",
"karma-start": "BABEL_ENV=karma karma start config/karma.config.js",
diff --git a/qa/README.md b/qa/README.md
index 8efdd8514f1..002ad4c65f5 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -49,8 +49,10 @@ will need to [modify your GDK setup](https://gitlab.com/gitlab-org/gitlab-qa/blo
### Writing tests
-1. [Using page objects](qa/page/README.md)
-2. [Style guide](STYLE_GUIDE.md)
+- [Writing tests from scratch tutorial](docs/WRITING_TESTS_FROM_SCRATCH.md)
+ - [Best practices](docs/BEST_PRACTICES.md)
+ - [Using page objects](qa/page/README.md)
+ - [Guidelines](docs/GUIDELINES.md)
### Running specific tests
diff --git a/qa/docs/BEST_PRACTICES.md b/qa/docs/BEST_PRACTICES.md
new file mode 100644
index 00000000000..a872b915926
--- /dev/null
+++ b/qa/docs/BEST_PRACTICES.md
@@ -0,0 +1,38 @@
+# Best practices when writing end-to-end tests
+
+The majority of the end-to-end tests require some state to be built in the application for the tests to happen.
+
+A good example is a user being logged in as a pre-condition for testing the feature.
+
+But if the login feature is already covered with end-to-end tests through the GUI, there is no reason to perform such an expensive task to test the functionality of creating a project, or importing a repo, even if this features depend on a user being logged in. Let's see an example to make things clear.
+
+Let's say that, on average, the process to perform a successfull login through the GUI takes 2 seconds.
+
+Now, realize that almost all tests need the user to be logged in, and that we need every test to run in isolation, meaning that tests cannot interfere with each other. This would mean that for every test the user needs to log in, and "waste 2 seconds".
+
+Now, multiply the number of tests per 2 seconds, and as your test suite grows, the time to run it grows with it, and this is not sustainable.
+
+An alternative to perform a login in a cheaper way would be having an endpoint (available only for testing) where we could pass the user's credentials as encrypted values as query strings, and then we would be redirected to the logged in home page if the credentials are valid. Let's say that, on average, this process takes only 200 miliseconds.
+
+You see the point right?
+
+Performing a login through the GUI for every test would cost a lot in terms of tests' execution.
+
+And there is another reason.
+
+Let's say that you don't follow the above suggestion, and depend on the GUI for the creation of every application state in order to test a specific feature. In this case we could be talking about the **Issues** feature, that depends on a project to exist, and the user to be logged in.
+
+What would happen if there was a bug in the project creation page, where the 'Create' button is disabled, not allowing for the creation of a project through the GUI, but the API logic is still working?
+
+In this case, instead of having only the project creation test failing, we would have many tests that depend on a project to be failing too.
+
+But, if we were following the best practices, only one test would be failing, and tests for other features that depend on a project to exist would continue to pass, since they could be creating the project behind the scenes interacting directly with the public APIs, ensuring a more reliable metric of test failure rate.
+
+Finally, interacting with the application only by its GUI generates a higher rate of test flakiness, and we want to avoid that at max.
+
+**The takeaways here are:**
+
+- Building state through the GUI is time consuming and it's not sustainable as the test suite grows.
+- When depending only on the GUI to create the application's state and tests fail due to front-end issues, we can't rely on the test failures rate, and we generates a higher rate of test flakiness.
+
+Now that we are aware of all of it, [let's go create some tests](./WRITING_TESTS_FROM_SCRATCH.md).
diff --git a/qa/STYLE_GUIDE.md b/qa/docs/GUIDELINES.md
index 900f7456e1a..9db52cd07e6 100644
--- a/qa/STYLE_GUIDE.md
+++ b/qa/docs/GUIDELINES.md
@@ -43,4 +43,4 @@ end
Notice that in the above example, before clicking the `:operations_environments_link`, another element is hovered over.
-> We can create these methods as helpers to abstract multi-step navigation. \ No newline at end of file
+> We can create these methods as helpers to abstract multi-step navigation.
diff --git a/qa/docs/WRITING_TESTS_FROM_SCRATCH.md b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
new file mode 100644
index 00000000000..a6daffc964e
--- /dev/null
+++ b/qa/docs/WRITING_TESTS_FROM_SCRATCH.md
@@ -0,0 +1,380 @@
+# Writing end-to-end tests step-by-step
+
+In this tutorial, you will find different examples, and the steps involved, in the creation of end-to-end (_e2e_) tests for GitLab CE and GitLab EE, using GitLab QA.
+
+> When referring to end-to-end tests in this document, this means testing a specific feature end-to-end, such as a user logging in, the creation of a project, the management of labels, breaking down epics into sub-epics and issues, etc.
+
+## Important information before we start writing tests
+
+It's important to understand that end-to-end tests of isolated features, such as the ones described in the above note, doesn't mean that everything needs to happen through the GUI.
+
+If you don't exactly understand what we mean by **not everything needs to happen through the GUI,** please make sure you've read the [best practices](./BEST_PRACTICES.md) before moving on.
+
+## This document covers the following items:
+
+0. Identifying if end-to-end tests are really needed
+1. Identifying the [DevOps stage](https://about.gitlab.com/stages-devops-lifecycle/) of the feature that you are going to cover with end-to-end tests
+2. Creating the skeleton of the test file (`*_spec.rb`)
+3. The [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of the test cases logic
+4. Extracting duplicated code into methods
+5. Tests' pre-conditions (`before :all` and `before`) using resources and [Page Objects](./qa/page/README.md)
+6. Optimizing the test suite
+7. Using and implementing resources
+8. Moving elements definitions and its methods to [Page Objects](./qa/page/README.md)
+ - Adding testability to the application
+
+### 0. Are end-to-end tests needed?
+
+At GitLab we respect the [test pyramid](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/development/testing_guide/testing_levels.md), and so, we recommend to check the code coverage of a specific feature before writing end-to-end tests.
+
+Sometimes you may notice that there is already a good coverage in other test levels, and we can stay confident that if we break a feature, we will still have quick feedback about it, even without having end-to-end tests.
+
+If after this analysis you still think that end-to-end tests are needed, keep reading.
+
+### 1. Identifying the DevOps stage
+
+The GitLab QA end-to-end tests are organized by the different [stages in the DevOps lifecycle](https://gitlab.com/gitlab-org/gitlab-ce/tree/master/qa/qa/specs/features/browser_ui), and so, if you are creating tests for issue creation, for instance, you would locate the spec files under the `qa/qa/specs/features/browser_ui/2_plan/` directory since issue creation is part of the Plan stage.
+
+ In another case of a test for listing merged merge requests (MRs), the test should go under the `qa/qa/specs/features/browser_ui/3_create/` directory since merge request is a feature from the Create stage.
+
+> There may be sub-directories inside the stages directories, for different features. For example: `.../browser_ui/2_plan/ee_epics/` and `.../browser_ui/2_plan/issues/`.
+
+Now, let's say we want to create tests for the [scoped labels](https://about.gitlab.com/2019/04/22/gitlab-11-10-released/#scoped-labels) feature, available on GitLab EE Premium (this feature is part of the Plan stage.)
+
+> Because these tests are for a feature available only on GitLab EE, we need to create them in the [EE repository](https://gitlab.com/gitlab-org/gitlab-ee).
+
+Since [there is no specific directory for this feature](https://gitlab.com/gitlab-org/gitlab-ee/tree/master/qa/qa/specs/features/browser_ui/2_plan), we should create a sub-directory for it.
+
+Under `.../browser_ui/2_plan/`, let's create a sub-directory called `ee_scoped_labels/`.
+
+> Notice that since this feature is only available for GitLab EE we prefix the sub-directory with `ee_`.
+
+### 2. Test skeleton
+
+Inside the newly created sub-directory, let's create a file describing the test suite (e.g. `editing_scoped_labels_spec.rb`.)
+
+#### The `context` and `describe` blocks
+
+Specs have an outer `context` that indicates the DevOps stage. The next level is the `describe` block, that briefly states the subject of the test suite. See the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ end
+ end
+end
+```
+
+#### The `it` blocks
+
+Every test suite is composed by at least one `it` block, and a good way to start writing end-to-end tests is by typing test cases descriptions as `it` blocks. Take a look at the following example:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ it 'replaces an existing label if it has the same key' do
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ end
+ end
+ end
+end
+```
+
+### 3. Test cases MVC
+
+For the [MVC](https://about.gitlab.com/handbook/values/#minimum-viable-change-mvc) of our test cases, let's say that we already have the application in the state needed for the tests, and then let's focus on the logic of the test cases only.
+
+To evolve the test cases drafted on step 2, let's imagine that the user is already logged in a GitLab EE instance, they already have at least a Premium license in use, there is already a project created, there is already an issue opened in the project, the issue already has a scoped label (e.g. `foo::bar`), there are other scoped labels (for the same scope and for a different scope, e.g. `foo::baz` and `bar::bah`), and finally, the user is already on the issue's page. Let's also suppose that for every test case the application is in a clean state, meaning that one test case won't affect another.
+
+> Note: there are different approaches to create an application state for end-to-end tests. Some of them are very time consuming and subject to failures, such as when using the GUI for all the pre-conditions of the tests. On the other hand, other approaches are more efficient, such as using the public APIs. The latter is more efficient since it doesn't depend on the GUI. We won't focus on this part yet, but it's good to keep it in mind.
+
+Let's now focus on the first test case.
+
+```ruby
+it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['foo::baz', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_label).to have_content('foo::baz')
+ expect(scoped_label).not_to have_content('foo::bar')
+ expect(page).to have_content('added foo::baz label and removed foo::bar')
+end
+```
+
+> Notice that the test itself is simple. The most challenging part is the creation of the application state, which will be covered later.
+
+> The exemplified test cases' MVC is not enough for the change to be submitted in an MR, but they help on building up the test logic. The reason is that we do not want to use locators directly in the tests, and tests **must** use [Page Objects](./qa/page/README.md) before they can be merged.
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it
+2. Then it fills in the 'Assign labels' input field with the value 'foo::baz' and press enter
+3. Then it clicks in the content body to apply the label and refreshes the page
+4. Finally the expectation that the previous scoped label was removed and that the new one was added happens
+
+Let's now see how the second test case would look like.
+
+```ruby
+it 'keeps both scoped labels when adding a label with a different key' do
+ # This implementation is only for tutorial purposes. We normally encapsulate elements in Page Objects.
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys ['bar::bah', :enter]
+ page.find('#content-body').click
+ page.refresh
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_labels.first).to have_content('bar::bah')
+ expect(scoped_labels.last).to have_content('foo::ba')
+ expect(page).to have_content('added bar::bah')
+ expect(page).to have_content('added foo::ba')
+end
+```
+
+> Note that elements are always located using CSS selectors, and a good practice is to add test specific attribute:value for elements (this is called adding testability to the application and we will talk more about it later.)
+
+Below are the steps that the test covers:
+
+1. The test finds the 'Edit' link for the labels and clicks on it
+2. Then it fills in the 'Assign labels' input field with the value 'bar::bah' and press enter
+3. Then it clicks in the content body to apply the label and refreshes the page
+4. Finally the expectation that the both scoped labels are present happens
+
+> Similar to the previous test, this one is also very straight forward, but there is some code duplication. Let's address it.
+
+### 4. Extracting duplicated code
+
+If we refactor the tests created on step 3 we could come up with something like this:
+
+```ruby
+it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ select_label_and_refresh 'foo::baz'
+
+ expect(page).to have_content('added foo::baz')
+ expect(page).to have_content('and removed foo::bar')
+
+ scoped_label = page.find('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_label).to have_content('foo::baz')
+ expect(scoped_label).not_to have_content('foo::bar')
+end
+
+it 'keeps both scoped label when adding a label with a different key' do
+ select_label_and_refresh 'bar::bah'
+
+ expect(page).to have_content('added bar::bah')
+ expect(page).to have_content('added foo::ba')
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(scoped_labels.first).to have_content('bar::bah')
+ expect(scoped_labels.last).to have_content('foo::ba')
+end
+
+def select_label_and_refresh(label)
+ page.find('.block.labels .edit-link').click
+ page.find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ page.find('#content-body').click
+ page.refresh
+end
+```
+
+By creating a reusable `select_label_and_refresh` method we remove the code duplication, and later we can move this method to a Page Object class that will be created for easier maintenance purposes.
+
+> Notice that the reusable method is created in the bottom of the file. The reason for that is that reading the code should be similar to reading a newspaper, where high-level information is at the top, like the title and summary of the news, while low level, or more specific information, is at the bottom.
+
+### 5. Tests' pre-conditions using resources and Page Objects
+
+In this section, we will address the previously mentioned subject of creating the application state for the tests, using the `before :all` and `before` blocks, together with resources and Page Objects.
+
+#### `before :all`
+
+A pre-condition for the entire test suite is defined in the `before :all` block.
+
+For our test suite example, some things that could happen before the entire test suite starts are:
+
+- The user logging in;
+- A premium license already being set up;
+- A project being created with an issue and labels already setup.
+
+> In case of a test suite with only one `it` block it's ok to use only the `before` block (see below) with all the test's pre-conditions.
+
+#### `before`
+
+A pre-condition for each test case is defined in the `before` block.
+
+For our test cases samples, what we need is that for every test the issue page is opened, and there is only one scoped label applied to it.
+
+#### Implementation
+
+In the following code we will focus on the test suite and the test cases' pre-conditions only:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ before :all do
+ project = Resource::Project.fabricate_via_api! do |resource|
+ resource.name = 'scoped-labels-project'
+ end
+
+ @foo_bar_scoped_label = 'foo::bar'
+
+ @issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.project = project
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @foo_bar_scoped_label
+ end
+
+ @labels = ['foo::baz', 'bar::bah']
+ @labels.each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = project.id
+ l.title = label
+ end
+ end
+
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ end
+
+ before do
+ Page::Project::Issue::Show.perform do |issue_page|
+ @issue.visit!
+ end
+ end
+
+ it 'keeps the latest scoped label when adding a label with the same key of an existing one, but with a different value' do
+ ...
+ end
+
+ it 'keeps both scoped labels when adding a label with a different key' do
+ ...
+ end
+
+ def select_label_and_refresh(label)
+ ...
+ end
+ end
+ end
+end
+```
+
+In the `before :all` block we create all the application state needed for the tests to run. We do that by fabricating resources via APIs (`project`, `@issue`, and `@labels`), by using the `Runtime::Browser.visit` method to go to the login page, and by performing a `sign_in_using_credentials` from the `Login` Page Object.
+
+> When creating the resources, notice that when calling the `fabricate_via_api` method, we pass some attribute:values, like `name` for the `project` resoruce, `project`, `title`, and `labels` for the the issue resource, and `project`, and `title` for `label` resources.
+
+> What's important to understand here is that by creating the application state mostly using the public APIs we save a lot of time in the test suite setup stage.
+
+> Soon we will cover the use of the already existing resources' methods and the creation of your own `fabricate_via_api` methods for resources where this is still not available, but first, let's optimize our implementation.
+
+### 6. Optimization
+
+As already mentioned in the [best practices](./BEST_PRACTICES.md) document, end-to-end tests are very costly in terms of execution time, and it's our responsibility as software engineers to ensure that we optimize them as max as possible.
+
+> Differently than unit tests, that exercise every little piece of the application in isolation, usually having only one assertion per test, and being very fast to run, end-to-end tests can have more actions and assertions in a single test to help on speeding up the test's feedback since they are much slower when comparing to unit tests.
+
+Some improvements that we could make in our test suite to optimize its time to run are:
+
+1. Having a single test case (an `it` block) that exercise both scenarios to avoid "wasting" time in the tests' pre-conditions, instead of having two different test cases.
+2. Moving all the pre-conditions to the `before` block since there will be only one `it` block.
+3. Making the selection of labels more performant by allowing for the selection of more than one label in the same reusable method.
+
+Let's look at a suggestion that addresses the above points, one by one:
+
+```ruby
+module QA
+ context 'Plan' do
+ describe 'Editing scoped labels properties on issues' do
+ before do
+ project = Resource::Project.fabricate_via_api! do |resource|
+ resource.name = 'scoped-labels-project'
+ end
+
+ @foo_bar_scoped_label = 'foo::bar'
+
+ @issue = Resource::Issue.fabricate_via_api! do |issue|
+ issue.project = project
+ issue.title = 'Issue to test the scoped labels'
+ issue.labels = @foo_bar_scoped_label
+ end
+
+ @labels = ['foo::baz', 'bar::bah']
+ @labels.each do |label|
+ Resource::Label.fabricate_via_api! do |l|
+ l.project = project.id
+ l.title = label
+ end
+ end
+
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.perform(&:sign_in_using_credentials)
+ Page::Project::Issue::Show.perform do |issue_page|
+ @issue.visit!
+ end
+ end
+
+ it 'correctly applies the scoped labels depending if they are from the same or a different scope' do
+ select_labels_and_refresh @labels
+
+ scoped_labels = page.all('.qa-labels-block .scoped-label-wrapper')
+
+ expect(page).to have_content("added #{@foo_bar_scoped_label}")
+ expect(page).to have_content("added #{@labels[1]} #{@labels[0]} labels and removed #{@foo_bar_scoped_label}")
+ expect(scoped_labels.count).to eq(2)
+ expect(scoped_labels.first).to have_content(@labels[1])
+ expect(scoped_labels.last).to have_content(@labels[0])
+ end
+
+ def select_labels_and_refresh(labels)
+ find('.block.labels .edit-link').click
+ labels.each do |label|
+ find('.dropdown-menu-labels .dropdown-input-field').send_keys [label, :enter]
+ end
+ find('#content-body').click
+ refresh
+ end
+ end
+ end
+ end
+```
+
+As you can see, now all the pre-conditions from the `before :all` block were moved to the `before` block, addressing point 2.
+
+To address point 1, we changed the test implementation from two `it` blocks into a single one that exercises both scenarios. Now the new test description is: `'correctly applies the scoped labels depending if they are from the same or a different scope'`. It's a long description, but it describes well what the test does.
+
+> Notice that the implementation of the new and unique `it` block had to change a little bit. Below we describe in details what it does.
+
+1. At the same time, it selects two scoped labels, one from the same scope of the one already applied in the issue during the setup phase (in the `before` block), and another one from a different scope.
+2. It runs the assertions that the labels where correctly added and removed; that only two labels are applied; and that those are the correct ones, and that they are shown in the right order.
+
+Finally, the `select_label_and_refresh` method is changed to `select_labels_and_refresh`, which accepts an array of labels instead of a single label, and it iterates on them for faster label selection (this is what is used in step 1 explained above.)
+
+### 7. Resources
+
+TBD.
+
+### 8. Page Objects
+
+> Page Objects are auto-loaded in the `qa/qa.rb` file and available in all the test files (`*_spec.rb`).
+
+Page Objects are used in end-to-end tests for maintenance reasons, where page's elements and methods are defined to be reused in any test.
+
+Take a look at [this document that specifically details the usage of Page Objects](./qa/page/README.md).
+
+Now, let's go back to our examples.
+
+...
+
+#### Adding testability
+
+TBD. \ No newline at end of file
diff --git a/qa/qa/page/base.rb b/qa/qa/page/base.rb
index 9fabf83e2ce..c395e5f6011 100644
--- a/qa/qa/page/base.rb
+++ b/qa/qa/page/base.rb
@@ -113,8 +113,8 @@ module QA
has_css?(element_selector_css(name), wait: wait, text: text)
end
- def has_no_element?(name, wait: Capybara.default_max_wait_time)
- has_no_css?(element_selector_css(name), wait: wait)
+ def has_no_element?(name, text: nil, wait: Capybara.default_max_wait_time)
+ has_no_css?(element_selector_css(name), wait: wait, text: text)
end
def has_text?(text)
@@ -129,8 +129,8 @@ module QA
has_no_css?('.fa-spinner', wait: Capybara.default_max_wait_time)
end
- def within_element(name)
- page.within(element_selector_css(name)) do
+ def within_element(name, text: nil)
+ page.within(element_selector_css(name), text: text) do
yield
end
end
diff --git a/qa/qa/page/project/branches/show.rb b/qa/qa/page/project/branches/show.rb
index 922a6ddb086..762eacdab15 100644
--- a/qa/qa/page/project/branches/show.rb
+++ b/qa/qa/page/project/branches/show.rb
@@ -7,6 +7,7 @@ module QA
class Show < Page::Base
view 'app/views/projects/branches/_branch.html.haml' do
element :remove_btn
+ element :branch_name
end
view 'app/views/projects/branches/_panel.html.haml' do
element :all_branches
@@ -27,11 +28,9 @@ module QA
finished_loading?
end
- def has_branch_title?(branch_title)
+ def has_no_branch?(branch_name)
within_element(:all_branches) do
- within(".item-title") do
- has_text?(branch_title)
- end
+ has_no_element?(:branch_name, text: branch_name, wait: Support::Waiter::DEFAULT_MAX_WAIT_TIME)
end
end
@@ -48,15 +47,6 @@ module QA
click_element(:delete_merged_branches)
end
end
-
- def wait_for_texts_not_to_be_visible(texts)
- text_not_visible = wait do
- texts.all? do |text|
- has_no_text?(text)
- end
- end
- raise "Expected text(s) #{texts} not to be visible" unless text_not_visible
- end
end
end
end
diff --git a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
index c2c2b6da90a..cf6c24fa873 100644
--- a/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
+++ b/qa/qa/specs/features/browser_ui/3_create/repository/add_list_delete_branches_spec.rb
@@ -73,10 +73,9 @@ module QA
Page::Project::Branches::Show.perform do |branches_view|
branches_view.delete_branch(third_branch)
+ expect(branches_view).to have_no_branch(third_branch)
end
- expect(page).not_to have_content(third_branch)
-
Page::Project::Branches::Show.perform(&:delete_merged_branches)
expect(page).to have_content(
@@ -85,8 +84,7 @@ module QA
page.refresh
Page::Project::Branches::Show.perform do |branches_view|
- branches_view.wait_for_texts_not_to_be_visible([commit_message_of_second_branch])
- expect(branches_view).not_to have_branch_title(second_branch)
+ expect(branches_view).to have_no_branch(second_branch)
end
end
end
diff --git a/qa/qa/support/page/logging.rb b/qa/qa/support/page/logging.rb
index 69b6332ecce..ff505fdbddd 100644
--- a/qa/qa/support/page/logging.rb
+++ b/qa/qa/support/page/logging.rb
@@ -76,23 +76,18 @@ module QA
super
end
- def has_element?(name, text: nil, wait: Capybara.default_max_wait_time)
+ def has_element?(name, **kwargs)
found = super
- msg = ["has_element? :#{name}"]
- msg << %Q(with text "#{text}") if text
- msg << "(wait: #{wait})"
- msg << "returned: #{found}"
-
- log(msg.compact.join(' '))
+ log_has_element_or_not('has_element?', name, found, **kwargs)
found
end
- def has_no_element?(name, wait: Capybara.default_max_wait_time)
+ def has_no_element?(name, **kwargs)
found = super
- log("has_no_element? :#{name} returned #{found}")
+ log_has_element_or_not('has_no_element?', name, found, **kwargs)
found
end
@@ -149,6 +144,15 @@ module QA
def log(msg)
QA::Runtime::Logger.debug(msg)
end
+
+ def log_has_element_or_not(method, name, found, **kwargs)
+ msg = ["#{method} :#{name}"]
+ msg << %Q(with text "#{kwargs[:text]}") if kwargs[:text]
+ msg << "(wait: #{kwargs[:wait] || Capybara.default_max_wait_time})"
+ msg << "returned: #{found}"
+
+ log(msg.compact.join(' '))
+ end
end
end
end
diff --git a/qa/qa/support/waiter.rb b/qa/qa/support/waiter.rb
index 21a399b4a5f..fdcf2d7e157 100644
--- a/qa/qa/support/waiter.rb
+++ b/qa/qa/support/waiter.rb
@@ -3,9 +3,11 @@
module QA
module Support
module Waiter
+ DEFAULT_MAX_WAIT_TIME = 60
+
module_function
- def wait(max: 60, interval: 0.1)
+ def wait(max: DEFAULT_MAX_WAIT_TIME, interval: 0.1)
QA::Runtime::Logger.debug("with wait: max #{max}; interval #{interval}")
start = Time.now
diff --git a/qa/spec/page/logging_spec.rb b/qa/spec/page/logging_spec.rb
index 707a7ff6d98..99e96b81a51 100644
--- a/qa/spec/page/logging_spec.rb
+++ b/qa/spec/page/logging_spec.rb
@@ -93,7 +93,14 @@ describe QA::Support::Page::Logging do
allow(page).to receive(:has_no_css?).and_return(true)
expect { subject.has_no_element?(:element) }
- .to output(/has_no_element\? :element returned true/).to_stdout_from_any_process
+ .to output(/has_no_element\? :element \(wait: 2\) returned: true/).to_stdout_from_any_process
+ end
+
+ it 'logs has_no_element? with text' do
+ allow(page).to receive(:has_no_css?).and_return(true)
+
+ expect { subject.has_no_element?(:element, text: "more text") }
+ .to output(/has_no_element\? :element with text \"more text\" \(wait: 2\) returned: true/).to_stdout_from_any_process
end
it 'logs has_text?' do
diff --git a/spec/controllers/concerns/project_unauthorized_spec.rb b/spec/controllers/concerns/project_unauthorized_spec.rb
index 57ac00cf4dd..5834b1ef37f 100644
--- a/spec/controllers/concerns/project_unauthorized_spec.rb
+++ b/spec/controllers/concerns/project_unauthorized_spec.rb
@@ -12,7 +12,7 @@ describe ProjectUnauthorized do
render_views
- describe '#project_unauthorized_proc' do
+ describe '.on_routable_not_found' do
controller(::Projects::ApplicationController) do
def show
head :ok
diff --git a/spec/controllers/concerns/routable_actions_spec.rb b/spec/controllers/concerns/routable_actions_spec.rb
new file mode 100644
index 00000000000..59d48c68b9c
--- /dev/null
+++ b/spec/controllers/concerns/routable_actions_spec.rb
@@ -0,0 +1,156 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe RoutableActions do
+ controller(::ApplicationController) do
+ include RoutableActions # rubocop:disable RSpec/DescribedClass
+
+ before_action :routable
+
+ def routable
+ @klass = params[:type].constantize
+ @routable = find_routable!(params[:type].constantize, params[:id])
+ end
+
+ def show
+ head :ok
+ end
+ end
+
+ def get_routable(routable)
+ get :show, params: { id: routable.full_path, type: routable.class }
+ end
+
+ describe '#find_routable!' do
+ context 'when signed in' do
+ let(:user) { create(:user) }
+
+ before do
+ sign_in(user)
+ end
+
+ context 'with a project' do
+ let(:routable) { create(:project) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the project' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Project)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a group' do
+ let(:routable) { create(:group, :private) }
+
+ context 'when authorized' do
+ before do
+ routable.add_guest(user)
+ end
+
+ it 'returns the group' do
+ get_routable(routable)
+
+ expect(assigns[:routable]).to be_a(Group)
+ end
+
+ it 'allows access' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+ end
+
+ it 'prevents access when not authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+
+ context 'with a user' do
+ let(:routable) { user }
+
+ it 'allows access when authorized' do
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(200)
+ end
+
+ it 'prevents access when unauthorized' do
+ allow(subject).to receive(:can?).and_return(false)
+
+ get_routable(user)
+
+ expect(response).to have_gitlab_http_status(404)
+ end
+ end
+ end
+
+ context 'when not signed in' do
+ it 'redirects to sign in for private resouces' do
+ routable = create(:project, :private)
+
+ get_routable(routable)
+
+ expect(response).to have_gitlab_http_status(302)
+ expect(response.location).to end_with('/users/sign_in')
+ end
+ end
+ end
+
+ describe '#perform_not_found_actions' do
+ let(:routable) { create(:project) }
+
+ before do
+ sign_in(create(:user))
+ end
+
+ it 'performs multiple checks' do
+ last_check_called = false
+ checks = [proc {}, proc { last_check_called = true }]
+ allow(subject).to receive(:not_found_actions).and_return(checks)
+
+ get_routable(routable)
+
+ expect(last_check_called).to eq(true)
+ end
+
+ it 'performs checks in the context of the controller' do
+ check = lambda { |routable| redirect_to routable }
+ allow(subject).to receive(:not_found_actions).and_return([check])
+
+ get_routable(routable)
+
+ expect(response.location).to end_with(routable.to_param)
+ end
+
+ it 'skips checks once one has resulted in a render/redirect' do
+ first_check = proc { render plain: 'first' }
+ second_check = proc { render plain: 'second' }
+ allow(subject).to receive(:not_found_actions).and_return([first_check, second_check])
+
+ get_routable(routable)
+
+ expect(response.body).to eq('first')
+ end
+ end
+end
diff --git a/spec/controllers/concerns/send_file_upload_spec.rb b/spec/controllers/concerns/send_file_upload_spec.rb
index 8408578a7db..a3ce08f736c 100644
--- a/spec/controllers/concerns/send_file_upload_spec.rb
+++ b/spec/controllers/concerns/send_file_upload_spec.rb
@@ -1,3 +1,4 @@
+# coding: utf-8
# frozen_string_literal: true
require 'spec_helper'
@@ -13,7 +14,7 @@ describe SendFileUpload do
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end
end
diff --git a/spec/factories/uploads.rb b/spec/factories/uploads.rb
index 7256f785e1f..426abdc2a6c 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -13,7 +13,7 @@ FactoryBot.define do
end
# this needs to comply with RecordsUpload::Concern#upload_path
- path { File.join("uploads/-/system", model.class.to_s.underscore, mount_point.to_s, 'avatar.jpg') }
+ path { File.join("uploads/-/system", model.class.underscore, mount_point.to_s, 'avatar.jpg') }
trait :personal_snippet_upload do
uploader "PersonalFileUploader"
diff --git a/spec/frontend/environment.js b/spec/frontend/environment.js
index 9612162ad0c..a8c8688441d 100644
--- a/spec/frontend/environment.js
+++ b/spec/frontend/environment.js
@@ -38,6 +38,16 @@ class CustomEnvironment extends JSDOMEnvironment {
this.global.fixturesBasePath = `${process.cwd()}/${
IS_EE ? 'ee/' : ''
}spec/javascripts/fixtures`;
+
+ // Not yet supported by JSDOM: https://github.com/jsdom/jsdom/issues/317
+ this.global.document.createRange = () => ({
+ setStart: () => {},
+ setEnd: () => {},
+ commonAncestorContainer: {
+ nodeName: 'BODY',
+ ownerDocument: this.global.document,
+ },
+ });
}
async teardown() {
diff --git a/spec/frontend/test_setup.js b/spec/frontend/test_setup.js
index c57e0e7cfc6..f0d62a3c41c 100644
--- a/spec/frontend/test_setup.js
+++ b/spec/frontend/test_setup.js
@@ -3,7 +3,7 @@ import * as jqueryMatchers from 'custom-jquery-matchers';
import Translate from '~/vue_shared/translate';
import axios from '~/lib/utils/axios_utils';
import { initializeTestTimeout } from './helpers/timeout';
-import { getJSONFixture, loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
+import { loadHTMLFixture, setHTMLFixture } from './helpers/fixtures';
process.on('unhandledRejection', global.promiseRejectionHandler);
@@ -46,9 +46,12 @@ Object.defineProperty(global.Element.prototype, 'innerText', {
// convenience wrapper for migration from Karma
Object.assign(global, {
loadFixtures: loadHTMLFixture,
- loadJSONFixtures: getJSONFixture,
- preloadFixtures() {},
setFixtures: setHTMLFixture,
+
+ // The following functions fill the fixtures cache in Karma.
+ // This is not necessary in Jest because we make no Ajax request.
+ loadJSONFixtures() {},
+ preloadFixtures() {},
});
// custom-jquery-matchers was written for an old Jest version, we need to make it compatible
diff --git a/spec/helpers/dashboard_helper_spec.rb b/spec/helpers/dashboard_helper_spec.rb
index 7ba24ba2956..023238ee0ae 100644
--- a/spec/helpers/dashboard_helper_spec.rb
+++ b/spec/helpers/dashboard_helper_spec.rb
@@ -21,4 +21,10 @@ describe DashboardHelper do
expect(helper.dashboard_nav_links).not_to include(:activity, :milestones)
end
end
+
+ describe '.has_start_trial?' do
+ subject { helper.has_start_trial? }
+
+ it { is_expected.to eq(false) }
+ end
end
diff --git a/spec/helpers/emails_helper_spec.rb b/spec/helpers/emails_helper_spec.rb
index 03b4c19ec22..0434af25866 100644
--- a/spec/helpers/emails_helper_spec.rb
+++ b/spec/helpers/emails_helper_spec.rb
@@ -1,6 +1,45 @@
require 'spec_helper'
describe EmailsHelper do
+ describe 'closure_reason_text' do
+ context 'when given a MergeRequest' do
+ let(:merge_request) { create(:merge_request) }
+ let(:merge_request_presenter) { merge_request.present }
+
+ context "and format is text" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+
+ context "and format is HTML" do
+ it "returns HTML" do
+ expect(closure_reason_text(merge_request, format: :html)).to eq(" via merge request #{link_to(merge_request.to_reference, merge_request_presenter.web_url)}")
+ end
+ end
+
+ context "and format is unknown" do
+ it "returns plain text" do
+ expect(closure_reason_text(merge_request, format: :text)).to eq(" via merge request #{merge_request.to_reference} (#{merge_request_presenter.web_url})")
+ end
+ end
+ end
+
+ context 'when given a String' do
+ let(:closed_via) { "5a0eb6fd7e0f133044378c662fcbbc0d0c16dbfa" }
+
+ it "returns plain text" do
+ expect(closure_reason_text(closed_via)).to eq(" via #{closed_via}")
+ end
+ end
+
+ context 'when not given anything' do
+ it "returns empty string" do
+ expect(closure_reason_text(nil)).to eq("")
+ end
+ end
+ end
+
describe 'sanitize_name' do
context 'when name contains a valid URL string' do
it 'returns name with `.` replaced with `_` to prevent mail clients from auto-linking URLs' do
diff --git a/spec/helpers/nav_helper_spec.rb b/spec/helpers/nav_helper_spec.rb
index e840c927d59..979d89812f5 100644
--- a/spec/helpers/nav_helper_spec.rb
+++ b/spec/helpers/nav_helper_spec.rb
@@ -50,4 +50,16 @@ describe NavHelper do
expect(helper.header_links).to contain_exactly(:sign_in, :search)
end
end
+
+ context '.admin_monitoring_nav_links' do
+ subject { helper.admin_monitoring_nav_links }
+
+ it { is_expected.to all(be_a(String)) }
+ end
+
+ context '.group_issues_sub_menu_items' do
+ subject { helper.group_issues_sub_menu_items }
+
+ it { is_expected.to all(be_a(String)) }
+ end
end
diff --git a/spec/helpers/projects_helper_spec.rb b/spec/helpers/projects_helper_spec.rb
index 554cb861563..83271aa24a3 100644
--- a/spec/helpers/projects_helper_spec.rb
+++ b/spec/helpers/projects_helper_spec.rb
@@ -799,4 +799,24 @@ describe ProjectsHelper do
it { is_expected.to eq(result) }
end
end
+
+ describe '#can_import_members?' do
+ let(:project) { create(:project) }
+ let(:user) { create(:user) }
+ let(:owner) { project.owner }
+
+ before do
+ helper.instance_variable_set(:@project, project)
+ end
+
+ it 'returns false if user cannot admin_project_member' do
+ allow(helper).to receive(:current_user) { user }
+ expect(helper.can_import_members?).to eq false
+ end
+
+ it 'returns true if user can admin_project_member' do
+ allow(helper).to receive(:current_user) { owner }
+ expect(helper.can_import_members?).to eq true
+ end
+ end
end
diff --git a/spec/javascripts/diffs/store/actions_spec.js b/spec/javascripts/diffs/store/actions_spec.js
index c82dcadd2f1..6309a8823d7 100644
--- a/spec/javascripts/diffs/store/actions_spec.js
+++ b/spec/javascripts/diffs/store/actions_spec.js
@@ -82,7 +82,7 @@ describe('DiffsStoreActions', () => {
describe('fetchDiffFiles', () => {
it('should fetch diff files', done => {
- const endpoint = '/fetch/diff/files';
+ const endpoint = '/fetch/diff/files?w=1';
const mock = new MockAdapter(axios);
const res = { diff_files: 1, merge_request_diffs: [] };
mock.onGet(endpoint).reply(200, res);
@@ -828,6 +828,10 @@ describe('DiffsStoreActions', () => {
});
describe('setShowWhitespace', () => {
+ beforeEach(() => {
+ spyOn(eventHub, '$emit').and.stub();
+ });
+
it('commits SET_SHOW_WHITESPACE', done => {
testAction(
setShowWhitespace,
@@ -855,6 +859,30 @@ describe('DiffsStoreActions', () => {
expect(window.history.pushState).toHaveBeenCalled();
});
+
+ it('calls history pushState with merged params', () => {
+ const originalPushState = window.history;
+
+ originalPushState.pushState({}, '', '?test=1');
+
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(window.history.pushState.calls.mostRecent().args[2]).toMatch(/(.*)\?test=1&w=0/);
+
+ originalPushState.pushState({}, '', '?');
+ });
+
+ it('emits eventHub event', () => {
+ spyOn(localStorage, 'setItem').and.stub();
+ spyOn(window.history, 'pushState').and.stub();
+
+ setShowWhitespace({ commit() {} }, { showWhitespace: true, pushState: true });
+
+ expect(eventHub.$emit).toHaveBeenCalledWith('refetchDiffData');
+ });
});
describe('setRenderIt', () => {
diff --git a/spec/javascripts/fly_out_nav_spec.js b/spec/javascripts/fly_out_nav_spec.js
index 7ef44f29c5b..4772f754937 100644
--- a/spec/javascripts/fly_out_nav_spec.js
+++ b/spec/javascripts/fly_out_nav_spec.js
@@ -14,6 +14,7 @@ import {
setSidebar,
subItemsMouseLeave,
} from '~/fly_out_nav';
+import { SIDEBAR_COLLAPSED_CLASS } from '~/contextual_sidebar';
import bp from '~/breakpoints';
describe('Fly out sidebar navigation', () => {
@@ -219,7 +220,7 @@ describe('Fly out sidebar navigation', () => {
it('shows collapsed only sub-items if icon only sidebar', () => {
const subItems = el.querySelector('.sidebar-sub-level-items');
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
subItems.classList.add('is-fly-out-only');
setSidebar(sidebar);
@@ -296,7 +297,7 @@ describe('Fly out sidebar navigation', () => {
it('returns true when active & collapsed sidebar', () => {
const sidebar = document.createElement('div');
- sidebar.classList.add('sidebar-collapsed-desktop');
+ sidebar.classList.add(SIDEBAR_COLLAPSED_CLASS);
el.classList.add('active');
setSidebar(sidebar);
diff --git a/spec/lib/gitlab/favicon_spec.rb b/spec/lib/gitlab/favicon_spec.rb
index 49a423191bb..dce56bbd2c4 100644
--- a/spec/lib/gitlab/favicon_spec.rb
+++ b/spec/lib/gitlab/favicon_spec.rb
@@ -7,7 +7,7 @@ RSpec.describe Gitlab::Favicon, :request_store do
expect(described_class.main).to match_asset_path '/assets/favicon.png'
end
- it 'has blue favicon for development' do
+ it 'has blue favicon for development', unless: Gitlab.ee? do
allow(Rails).to receive(:env).and_return(ActiveSupport::StringInquirer.new('development'))
expect(described_class.main).to match_asset_path '/assets/favicon-blue.png'
end
diff --git a/spec/lib/gitlab/lets_encrypt/challenge_spec.rb b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
new file mode 100644
index 00000000000..74622f356de
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/challenge_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Challenge do
+ delegated_methods = {
+ url: 'https://example.com/',
+ status: 'pending',
+ token: 'tokenvalue',
+ file_content: 'hereisfilecontent',
+ request_validation: true
+ }
+
+ let(:acme_challenge) do
+ acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+ allow(acme_challenge).to receive_messages(delegated_methods)
+ acme_challenge
+ end
+
+ let(:challenge) { described_class.new(acme_challenge) }
+
+ delegated_methods.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Challenge' do
+ expect(challenge.public_send(method)).to eq(value)
+ end
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/client_spec.rb b/spec/lib/gitlab/lets_encrypt/client_spec.rb
new file mode 100644
index 00000000000..16a16acfd25
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/client_spec.rb
@@ -0,0 +1,120 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Client do
+ include LetsEncryptHelpers
+
+ let(:client) { described_class.new }
+
+ before do
+ stub_application_setting(
+ lets_encrypt_notification_email: 'myemail@test.example.com',
+ lets_encrypt_terms_of_service_accepted: true
+ )
+ end
+
+ let!(:stub_client) { stub_lets_encrypt_client }
+
+ shared_examples 'ensures account registration' do
+ it 'ensures account registration' do
+ subject
+
+ expect(stub_client).to have_received(:new_account).with(
+ contact: 'mailto:myemail@test.example.com',
+ terms_of_service_agreed: true
+ )
+ end
+
+ context 'when acme integration is disabled' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it 'raises error' do
+ expect do
+ subject
+ end.to raise_error('Acme integration is disabled')
+ end
+ end
+ end
+
+ describe '#new_order' do
+ subject(:new_order) { client.new_order('example.com') }
+
+ before do
+ order_double = instance_double('Acme::Order')
+ allow(stub_client).to receive(:new_order).and_return(order_double)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'returns order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_order' do
+ let(:url) { 'https://example.com/order' }
+ subject { client.load_order(url) }
+
+ before do
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(stub_client).to receive(:order).with(url: url).and_return(acme_order)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads order' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Order)
+ end
+ end
+
+ describe '#load_challenge' do
+ let(:url) { 'https://example.com/challenge' }
+ subject { client.load_challenge(url) }
+
+ before do
+ acme_challenge = instance_double('Acme::Client::Resources::Challenge')
+ allow(stub_client).to receive(:challenge).with(url: url).and_return(acme_challenge)
+ end
+
+ include_examples 'ensures account registration'
+
+ it 'loads challenge' do
+ is_expected.to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+
+ describe '#enabled?' do
+ subject { client.enabled? }
+
+ context 'when terms of service are accepted' do
+ it { is_expected.to eq(true) }
+
+ context 'when feature flag is disabled' do
+ before do
+ stub_feature_flags(pages_auto_ssl: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ context 'when terms of service are not accepted' do
+ before do
+ stub_application_setting(lets_encrypt_terms_of_service_accepted: false)
+ end
+
+ it { is_expected.to eq(false) }
+ end
+ end
+
+ describe '#terms_of_service_url' do
+ subject { client.terms_of_service_url }
+
+ it 'returns valid url' do
+ is_expected.to eq("https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf")
+ end
+ end
+end
diff --git a/spec/lib/gitlab/lets_encrypt/order_spec.rb b/spec/lib/gitlab/lets_encrypt/order_spec.rb
new file mode 100644
index 00000000000..ee7058baf8d
--- /dev/null
+++ b/spec/lib/gitlab/lets_encrypt/order_spec.rb
@@ -0,0 +1,39 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe ::Gitlab::LetsEncrypt::Order do
+ delegated_methods = {
+ url: 'https://example.com/',
+ status: 'valid'
+ }
+
+ let(:acme_order) do
+ acme_order = instance_double('Acme::Client::Resources::Order')
+ allow(acme_order).to receive_messages(delegated_methods)
+ acme_order
+ end
+
+ let(:order) { described_class.new(acme_order) }
+
+ delegated_methods.each do |method, value|
+ describe "##{method}" do
+ it 'delegates to Acme::Client::Resources::Order' do
+ expect(order.public_send(method)).to eq(value)
+ end
+ end
+ end
+
+ describe '#new_challenge' do
+ before do
+ challenge = instance_double('Acme::Client::Resources::Challenges::HTTP01')
+ authorization = instance_double('Acme::Client::Resources::Authorization')
+ allow(authorization).to receive(:http).and_return(challenge)
+ allow(acme_order).to receive(:authorizations).and_return([authorization])
+ end
+
+ it 'returns challenge' do
+ expect(order.new_challenge).to be_a(::Gitlab::LetsEncrypt::Challenge)
+ end
+ end
+end
diff --git a/spec/lib/gitlab/sentry_spec.rb b/spec/lib/gitlab/sentry_spec.rb
index ae522a588ee..af8b059b984 100644
--- a/spec/lib/gitlab/sentry_spec.rb
+++ b/spec/lib/gitlab/sentry_spec.rb
@@ -2,12 +2,15 @@ require 'spec_helper'
describe Gitlab::Sentry do
describe '.context' do
- it 'adds the locale to the tags' do
+ it 'adds the expected tags' do
expect(described_class).to receive(:enabled?).and_return(true)
+ allow(Labkit::Correlation::CorrelationId).to receive(:current_id).and_return('cid')
described_class.context(nil)
expect(Raven.tags_context[:locale].to_s).to eq(I18n.locale.to_s)
+ expect(Raven.tags_context[Labkit::Correlation::CorrelationId::LOG_KEY.to_sym].to_s)
+ .to eq('cid')
end
end
diff --git a/spec/models/active_session_spec.rb b/spec/models/active_session_spec.rb
index b523f393ece..2762eaeccd3 100644
--- a/spec/models/active_session_spec.rb
+++ b/spec/models/active_session_spec.rb
@@ -88,6 +88,52 @@ RSpec.describe ActiveSession, :clean_gitlab_redis_shared_state do
end
end
+ describe '.list_sessions' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+
+ redis.sadd(
+ "session:lookup:user:gitlab:#{user.id}",
+ %w[
+ 6919a6f1bb119dd7396fadc38fd18d0d
+ 59822c7d9fcdfa03725eff41782ad97d
+ ]
+ )
+ end
+
+ expect(ActiveSession.list_sessions(user)).to eq [{ _csrf_token: 'abcd' }]
+ end
+ end
+
+ describe '.session_ids_for_user' do
+ it 'uses the user lookup table to return session ids' do
+ session_ids = ['59822c7d9fcdfa03725eff41782ad97d']
+
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.sadd("session:lookup:user:gitlab:#{user.id}", session_ids)
+ end
+
+ expect(ActiveSession.session_ids_for_user(user)).to eq(session_ids)
+ end
+ end
+
+ describe '.sessions_from_ids' do
+ it 'uses the ActiveSession lookup to return original sessions' do
+ Gitlab::Redis::SharedState.with do |redis|
+ redis.set("session:gitlab:6919a6f1bb119dd7396fadc38fd18d0d", Marshal.dump({ _csrf_token: 'abcd' }))
+ end
+
+ expect(ActiveSession.sessions_from_ids(['6919a6f1bb119dd7396fadc38fd18d0d'])).to eq [{ _csrf_token: 'abcd' }]
+ end
+
+ it 'avoids a redis lookup for an empty array' do
+ expect(Gitlab::Redis::SharedState).not_to receive(:with)
+
+ expect(ActiveSession.sessions_from_ids([])).to eq([])
+ end
+ end
+
describe '.set' do
it 'sets a new redis entry for the user session and a lookup entry' do
ActiveSession.set(user, request)
diff --git a/spec/models/application_record_spec.rb b/spec/models/application_record_spec.rb
index cc90a998d3f..74573d0941c 100644
--- a/spec/models/application_record_spec.rb
+++ b/spec/models/application_record_spec.rb
@@ -52,4 +52,10 @@ describe ApplicationRecord do
expect { Suggestion.find_or_create_by!(note: nil) }.to raise_error(ActiveRecord::RecordInvalid)
end
end
+
+ describe '.underscore' do
+ it 'returns the underscored value of the class as a string' do
+ expect(MergeRequest.underscore).to eq('merge_request')
+ end
+ end
end
diff --git a/spec/models/event_spec.rb b/spec/models/event_spec.rb
index e91b5c4c86f..62663c247d1 100644
--- a/spec/models/event_spec.rb
+++ b/spec/models/event_spec.rb
@@ -88,7 +88,7 @@ describe Event do
let(:event) { create_push_event(project, user) }
it do
- expect(event.push?).to be_truthy
+ expect(event.push_action?).to be_truthy
expect(event.visible_to_user?(user)).to be_truthy
expect(event.visible_to_user?(nil)).to be_falsey
expect(event.tag?).to be_falsey
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 2a17bd6002e..425096d7e80 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -214,6 +214,13 @@ describe Project do
expect(project2).not_to be_valid
end
+ it 'validates the visibility' do
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_as_fork).and_call_original
+ expect_any_instance_of(described_class).to receive(:visibility_level_allowed_by_group).and_call_original
+
+ create(:project)
+ end
+
describe 'wiki path conflict' do
context "when the new path has been used by the wiki of other Project" do
it 'has an error on the name attribute' do
diff --git a/spec/models/push_event_spec.rb b/spec/models/push_event_spec.rb
index f86500f91cd..5509ed87308 100644
--- a/spec/models/push_event_spec.rb
+++ b/spec/models/push_event_spec.rb
@@ -123,9 +123,9 @@ describe PushEvent do
end
end
- describe '#push?' do
+ describe '#push_action?' do
it 'returns true' do
- expect(event).to be_push
+ expect(event).to be_push_action
end
end
diff --git a/spec/requests/api/helpers_spec.rb b/spec/requests/api/helpers_spec.rb
index 25a312cb734..ed907841bd8 100644
--- a/spec/requests/api/helpers_spec.rb
+++ b/spec/requests/api/helpers_spec.rb
@@ -247,9 +247,8 @@ describe API::Helpers do
exception = RuntimeError.new('test error')
allow(exception).to receive(:backtrace).and_return(caller)
- expect(Raven).to receive(:capture_exception).with(exception, tags: {
- correlation_id: 'new-correlation-id'
- }, extra: {})
+ expect(Raven).to receive(:capture_exception).with(exception, tags:
+ a_hash_including(correlation_id: 'new-correlation-id'), extra: {})
Labkit::Correlation::CorrelationId.use_id('new-correlation-id') do
handle_api_exception(exception)
diff --git a/spec/services/issues/close_service_spec.rb b/spec/services/issues/close_service_spec.rb
index fce9eed8b08..6874a8a0929 100644
--- a/spec/services/issues/close_service_spec.rb
+++ b/spec/services/issues/close_service_spec.rb
@@ -3,11 +3,13 @@
require 'spec_helper'
describe Issues::CloseService do
- let(:user) { create(:user) }
- let(:user2) { create(:user) }
+ let(:project) { create(:project, :repository) }
+ let(:user) { create(:user, email: "user@example.com") }
+ let(:user2) { create(:user, email: "user2@example.com") }
let(:guest) { create(:user) }
- let(:issue) { create(:issue, assignees: [user2], author: create(:user)) }
- let(:project) { issue.project }
+ let(:issue) { create(:issue, title: "My issue", project: project, assignees: [user2], author: create(:user)) }
+ let(:closing_merge_request) { create(:merge_request, source_project: project) }
+ let(:closing_commit) { create(:commit, project: project) }
let!(:todo) { create(:todo, :assigned, user: user, project: project, target: issue, author: user2) }
before do
@@ -39,7 +41,7 @@ describe Issues::CloseService do
.and_return(true)
expect(service).to receive(:close_issue)
- .with(issue, commit: nil, notifications: true, system_note: true)
+ .with(issue, closed_via: nil, notifications: true, system_note: true)
service.execute(issue)
end
@@ -57,6 +59,38 @@ describe Issues::CloseService do
end
describe '#close_issue' do
+ context "closed by a merge request" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_merge_request)
+ end
+ end
+
+ it 'mentions closure via a merge request' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_merge_request.to_reference))
+ end
+ end
+
+ context "closed by a commit" do
+ before do
+ perform_enqueued_jobs do
+ described_class.new(project, user).close_issue(issue, closed_via: closing_commit)
+ end
+ end
+
+ it 'mentions closure via a commit' do
+ email = ActionMailer::Base.deliveries.last
+
+ expect(email.to.first).to eq(user2.email)
+ expect(email.subject).to include(issue.title)
+ expect(email.body.parts.map(&:body)).to all(include(closing_commit.id))
+ end
+ end
+
context "valid params" do
before do
perform_enqueued_jobs do
diff --git a/spec/services/merge_requests/rebase_service_spec.rb b/spec/services/merge_requests/rebase_service_spec.rb
index a443e4588d9..7e2f03d1097 100644
--- a/spec/services/merge_requests/rebase_service_spec.rb
+++ b/spec/services/merge_requests/rebase_service_spec.rb
@@ -38,6 +38,32 @@ describe MergeRequests::RebaseService do
end
end
+ shared_examples 'sequence of failure and success' do
+ it 'properly clears the error message' do
+ allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq described_class::REBASE_ERROR
+
+ allow(repository).to receive(:gitaly_operation_client).and_call_original
+
+ service.execute(merge_request)
+
+ expect(merge_request.reload.merge_error).to eq nil
+ end
+ end
+
+ it_behaves_like 'sequence of failure and success'
+
+ context 'with deprecated step rebase feature' do
+ before do
+ stub_feature_flags(two_step_rebase: false)
+ end
+
+ it_behaves_like 'sequence of failure and success'
+ end
+
context 'when unexpected error occurs' do
before do
allow(repository).to receive(:gitaly_operation_client).and_raise('Something went wrong')
diff --git a/spec/support/helpers/features/notes_helpers.rb b/spec/support/helpers/features/notes_helpers.rb
index 38f30a14409..8a139fafac2 100644
--- a/spec/support/helpers/features/notes_helpers.rb
+++ b/spec/support/helpers/features/notes_helpers.rb
@@ -25,12 +25,10 @@ module Spec
page.within('.js-main-target-form') do
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
+ # Wait for quick action prompt to load and then dismiss it with ESC
+ # because it may block the Preview button
+ wait_for_requests
+ filled_text.send_keys(:escape)
click_on('Preview')
diff --git a/spec/support/helpers/graphql_helpers.rb b/spec/support/helpers/graphql_helpers.rb
index f15944652fd..44ed9da25fc 100644
--- a/spec/support/helpers/graphql_helpers.rb
+++ b/spec/support/helpers/graphql_helpers.rb
@@ -197,3 +197,7 @@ module GraphqlHelpers
allow(GitlabSchema).to receive(:max_query_depth).with(any_args).and_return nil
end
end
+
+# This warms our schema, doing this as part of loading the helpers to avoid
+# duplicate loading error when Rails tries autoload the types.
+GitlabSchema.graphql_definition
diff --git a/spec/support/helpers/lets_encrypt_helpers.rb b/spec/support/helpers/lets_encrypt_helpers.rb
new file mode 100644
index 00000000000..7f0886b451c
--- /dev/null
+++ b/spec/support/helpers/lets_encrypt_helpers.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module LetsEncryptHelpers
+ def stub_lets_encrypt_client
+ client = instance_double('Acme::Client')
+
+ allow(client).to receive(:new_account)
+ allow(client).to receive(:terms_of_service).and_return(
+ "https://letsencrypt.org/documents/LE-SA-v1.2-November-15-2017.pdf"
+ )
+
+ allow(Acme::Client).to receive(:new).with(
+ private_key: kind_of(OpenSSL::PKey::RSA),
+ directory: ::Gitlab::LetsEncrypt::Client::STAGING_DIRECTORY_URL
+ ).and_return(client)
+
+ client
+ end
+end
diff --git a/spec/uploaders/object_storage_spec.rb b/spec/uploaders/object_storage_spec.rb
index a62830c35f1..6bad5d49b1c 100644
--- a/spec/uploaders/object_storage_spec.rb
+++ b/spec/uploaders/object_storage_spec.rb
@@ -12,7 +12,7 @@ class Implementation < GitlabUploader
# user/:id
def dynamic_segment
- File.join(model.class.to_s.underscore, model.id.to_s)
+ File.join(model.class.underscore, model.id.to_s)
end
end