summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile.lock2
-rw-r--r--README.md2
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js7
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue64
-rw-r--r--app/assets/javascripts/jobs/components/job_log_controllers.vue139
-rw-r--r--app/assets/javascripts/jobs/components/jobs_container.vue60
-rw-r--r--app/assets/javascripts/jobs/components/stages_dropdown.vue97
-rw-r--r--app/assets/javascripts/jobs/components/trigger_block.vue84
-rw-r--r--app/assets/javascripts/locale/ensure_single_line.js25
-rw-r--r--app/assets/javascripts/locale/index.js13
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/index.js (renamed from app/assets/javascripts/pages/admin/cohorts/index.js)0
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js (renamed from app/assets/javascripts/pages/admin/cohorts/usage_ping.js)0
-rw-r--r--app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js (renamed from app/assets/javascripts/pages/admin/conversational_development_index/show/index.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue10
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss19
-rw-r--r--app/assets/stylesheets/framework/files.scss10
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss18
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss29
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/assets/stylesheets/pages/settings.scss8
-rw-r--r--app/assets/stylesheets/pages/todos.scss6
-rw-r--r--app/assets/stylesheets/pages/tree.scss8
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/concerns/awardable.rb2
-rw-r--r--app/models/concerns/fast_destroy_all.rb2
-rw-r--r--app/models/internal_id.rb6
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/project.rb8
-rw-r--r--app/models/project_auto_devops.rb6
-rw-r--r--app/models/site_statistic.rb2
-rw-r--r--app/models/user.rb2
-rw-r--r--app/services/groups/destroy_service.rb7
-rw-r--r--app/services/labels/promote_service.rb2
-rw-r--r--app/services/milestones/promote_service.rb2
-rw-r--r--app/services/projects/destroy_service.rb3
-rw-r--r--app/services/projects/move_deploy_keys_projects_service.rb2
-rw-r--r--app/services/projects/move_lfs_objects_projects_service.rb2
-rw-r--r--app/services/projects/move_notification_settings_service.rb2
-rw-r--r--app/services/projects/move_project_group_links_service.rb2
-rw-r--r--app/services/projects/move_project_members_service.rb2
-rw-r--r--app/services/projects/update_service.rb2
-rw-r--r--app/services/protected_branches/legacy_api_update_service.rb4
-rw-r--r--app/services/users/destroy_service.rb7
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/users/_access_levels.html.haml12
-rw-r--r--app/views/admin/users/_form.html.haml33
-rw-r--r--app/views/instance_statistics/cohorts/_cohorts_table.html.haml2
-rw-r--r--app/views/instance_statistics/conversational_development_index/_no_data.html.haml2
-rw-r--r--app/views/instance_statistics/conversational_development_index/index.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/show.html.haml3
-rw-r--r--app/workers/detect_repository_languages_worker.rb2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb2
-rw-r--r--changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml5
-rw-r--r--changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml5
-rw-r--r--changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml5
-rw-r--r--changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml5
-rw-r--r--changelogs/unreleased/50101-builds-dropdown.yml6
-rw-r--r--changelogs/unreleased/50101-commit-block.yml5
-rw-r--r--changelogs/unreleased/50101-trigger.yml5
-rw-r--r--changelogs/unreleased/50101-truncated-job-information.yml5
-rw-r--r--changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml5
-rw-r--r--changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml5
-rw-r--r--changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml5
-rw-r--r--changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml5
-rw-r--r--changelogs/unreleased/emoji-cutoff-1px.yml5
-rw-r--r--changelogs/unreleased/rails5-verbose-query-logs.yml5
-rw-r--r--changelogs/unreleased/repopulate_site_statistics.yml5
-rw-r--r--changelogs/unreleased/rouge_3-2-1.yml5
-rw-r--r--changelogs/unreleased/sh-bump-gitaly-for-11-2.yml5
-rw-r--r--config/gitlab.yml.example3
-rw-r--r--config/initializers/active_record_verbose_query_logs.rb4
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb15
-rw-r--r--db/migrate/20160712171823_remove_award_emojis_with_no_user.rb2
-rw-r--r--db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb47
-rw-r--r--db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb32
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/high_availability/nfs.md4
-rw-r--r--doc/administration/index.md2
-rw-r--r--doc/administration/job_traces.md54
-rw-r--r--doc/administration/repository_storage_types.md2
-rw-r--r--doc/ci/docker/README.md1
-rw-r--r--doc/ci/docker/using_kaniko.md60
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/development/README.md8
-rw-r--r--doc/development/ee_features.md28
-rw-r--r--doc/development/understanding_explain_plans.md676
-rw-r--r--doc/topics/autodevops/index.md5
-rw-r--r--doc/user/admin_area/img/cohorts.pngbin439635 -> 0 bytes
-rw-r--r--doc/user/admin_area/monitoring/convdev.md32
-rw-r--r--doc/user/admin_area/monitoring/img/convdev_index.pngbin119743 -> 0 bytes
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md5
-rw-r--r--doc/user/admin_area/settings/usage_statistics.md13
-rw-r--r--doc/user/admin_area/user_cohorts.md40
-rw-r--r--doc/user/index.md4
-rw-r--r--doc/user/instance_statistics/convdev.md26
-rw-r--r--doc/user/instance_statistics/img/cohorts.pngbin0 -> 59494 bytes
-rw-r--r--doc/user/instance_statistics/img/convdev_index.pngbin0 -> 316893 bytes
-rw-r--r--doc/user/instance_statistics/img/instance_statistics_button.pngbin0 -> 9462 bytes
-rw-r--r--doc/user/instance_statistics/index.md19
-rw-r--r--doc/user/instance_statistics/user_cohorts.md27
-rw-r--r--doc/user/markdown.md15
-rw-r--r--doc/user/project/import/img/manifest_upload.pngbin12079 -> 0 bytes
-rw-r--r--doc/user/project/import/index.md2
-rw-r--r--doc/user/project/import/manifest.md64
-rw-r--r--doc/user/project/integrations/hangouts_chat.md2
-rw-r--r--doc/user/project/pages/getting_started_part_three.md40
-rw-r--r--doc/user/project/pages/img/dns_add_new_a_record_example_updated.pngbin10578 -> 0 bytes
-rw-r--r--doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.pngbin0 -> 7704 bytes
-rw-r--r--doc/user/project/repository/img/repository_languages.pngbin0 -> 88244 bytes
-rw-r--r--doc/user/project/repository/index.md10
-rw-r--r--lib/gitlab/git/repository.rb10
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb4
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb6
-rw-r--r--lib/gitlab/github_import/importer/milestones_importer.rb12
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb8
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/tasks/gitlab/site_statistics.rake23
-rw-r--r--lib/tasks/gitlab/traces.rake17
-rw-r--r--locale/gitlab.pot212
-rw-r--r--package.json2
-rw-r--r--rubocop/cop/destroy_all.rb22
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--scripts/frontend/extract_gettext_all.js72
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/features/instance_statistics/cohorts_spec.rb8
-rw-r--r--spec/features/instance_statistics/conversational_development_index_spec.rb10
-rw-r--r--spec/javascripts/boards/board_list_spec.js9
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js20
-rw-r--r--spec/javascripts/jobs/commit_block_spec.js73
-rw-r--r--spec/javascripts/jobs/components/job_log_controllers_spec.js217
-rw-r--r--spec/javascripts/jobs/jobs_container_spec.js126
-rw-r--r--spec/javascripts/jobs/stages_dropdown_spec.js63
-rw-r--r--spec/javascripts/jobs/trigger_value_spec.js66
-rw-r--r--spec/javascripts/locale/ensure_single_line_spec.js35
-rw-r--r--spec/javascripts/notes/mock_data.js2
-rw-r--r--spec/javascripts/vue_shared/translate_spec.js213
-rw-r--r--spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb2
-rw-r--r--spec/lib/gitlab/bare_repository_import/importer_spec.rb4
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb10
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb9
-rw-r--r--spec/migrations/delete_inconsistent_internal_id_records_spec.rb119
-rw-r--r--spec/migrations/migrate_null_wiki_access_levels_spec.rb29
-rw-r--r--spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb2
-rw-r--r--spec/models/fork_network_member_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/project_auto_devops_spec.rb6
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_spec.rb48
-rw-r--r--spec/policies/group_policy_spec.rb2
-rw-r--r--spec/requests/api/project_hooks_spec.rb5
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb105
-rw-r--r--spec/rubocop/cop/destroy_all_spec.rb43
-rw-r--r--spec/services/groups/destroy_service_spec.rb11
-rw-r--r--spec/services/merge_requests/create_service_spec.rb2
-rw-r--r--spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb2
-rw-r--r--spec/services/projects/detect_repository_languages_service_spec.rb4
-rw-r--r--spec/services/todo_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb39
-rw-r--r--spec/spec_helper.rb8
-rw-r--r--spec/support/api/milestones_shared_examples.rb8
-rw-r--r--spec/support/shared_examples/fast_destroy_all.rb4
-rw-r--r--spec/tasks/gitlab/site_statistics_rake_spec.rb24
-rw-r--r--spec/tasks/gitlab/traces_rake_spec.rb112
-rw-r--r--spec/workers/project_destroy_worker_spec.rb7
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb2
-rw-r--r--yarn.lock80
193 files changed, 3556 insertions, 570 deletions
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a38b3bd31b1..90bdef2ea8c 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.117.0
+0.117.1
diff --git a/Gemfile.lock b/Gemfile.lock
index b33dd75c278..1aadc3fd0b6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -743,7 +743,7 @@ GEM
retriable (3.1.1)
rinku (2.0.0)
rotp (2.1.2)
- rouge (3.2.0)
+ rouge (3.2.1)
rqrcode (0.7.0)
chunky_png
rqrcode-rails3 (0.1.7)
diff --git a/README.md b/README.md
index b6e1cc9a432..96d8f709274 100644
--- a/README.md
+++ b/README.md
@@ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
-All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
+All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
## Install a development environment
diff --git a/app/assets/javascripts/boards/components/board_list.vue b/app/assets/javascripts/boards/components/board_list.vue
index 3e610a4088c..bfc8d9b03ad 100644
--- a/app/assets/javascripts/boards/components/board_list.vue
+++ b/app/assets/javascripts/boards/components/board_list.vue
@@ -203,7 +203,7 @@ export default {
this.showIssueForm = !this.showIssueForm;
},
onScroll() {
- if (!this.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
+ if (!this.list.loadingMore && (this.scrollTop() > this.scrollHeight() - this.scrollOffset)) {
this.loadNextPage();
}
},
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f9badb01535..f55aa843444 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -133,7 +133,6 @@ export default {
.then(() =>
this.getRawFileData({
path: this.file.path,
- baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}),
)
.then(() => {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c9795750d65..28b9d0df201 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -92,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
-export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
@@ -100,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
.then(raw => {
if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) {
+ const baseSha =
+ (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
+
service
.getBaseRawFileData(file, baseSha)
.then(baseRaw => {
@@ -122,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
action: payload =>
dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
- actionPayload: { path, baseSha },
+ actionPayload: { path },
});
reject();
});
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 0035d809062..eda8cdad908 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -87,7 +87,7 @@ class ImporterStatus {
details = error.response.data.errors;
}
- flash(__(`An error occurred while importing project: ${details}`));
+ flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
}
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
new file mode 100644
index 00000000000..7f485295513
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -0,0 +1,64 @@
+<script>
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ },
+ props: {
+ pipelineShortSha: {
+ type: String,
+ required: true,
+ },
+ pipelineShaPath: {
+ type: String,
+ required: true,
+ },
+ mergeRequestReference: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ mergeRequestPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ gitCommitTitlte: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="block">
+ <p>
+ {{ __('Commit') }}
+
+ <a
+ :href="pipelineShaPath"
+ class="js-commit-sha commit-sha link-commit"
+ >
+ {{ pipelineShortSha }}
+ </a>
+
+ <clipboard-button
+ :text="pipelineShortSha"
+ :title="__('Copy commit SHA to clipboard')"
+ />
+
+ <a
+ v-if="mergeRequestPath && mergeRequestReference"
+ :href="mergeRequestPath"
+ class="js-link-commit link-commit"
+ >
+ {{ mergeRequestReference }}
+ </a>
+ </p>
+
+ <p class="build-light-text append-bottom-0">
+ {{ gitCommitTitlte }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/job_log_controllers.vue b/app/assets/javascripts/jobs/components/job_log_controllers.vue
new file mode 100644
index 00000000000..513851e376f
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/job_log_controllers.vue
@@ -0,0 +1,139 @@
+<script>
+ import Icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
+ import { numberToHumanSize } from '~/lib/utils/number_utils';
+ import { s__, sprintf } from '~/locale';
+
+ export default {
+ components: {
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ canEraseJob: {
+ type: Boolean,
+ required: true,
+ },
+ size: {
+ type: Number,
+ required: true,
+ },
+ rawTracePath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ canScrollToTop: {
+ type: Boolean,
+ required: true,
+ },
+ canScrollToBottom: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ computed: {
+ jobLogSize() {
+ return sprintf('Showing last %{startSpanTag} %{size} %{endSpanTag} of log -', {
+ startSpanTag: '<span class="s-truncated-info-size truncated-info-size">',
+ endSpanTag: '</span>',
+ size: numberToHumanSize(this.size),
+ });
+ },
+ },
+ methods: {
+ handleEraseJobClick() {
+ // eslint-disable-next-line no-alert
+ if (window.confirm(s__('Job|Are you sure you want to erase this job?'))) {
+ this.$emit('eraseJob');
+ }
+ },
+ handleScrollToTop() {
+ this.$emit('scrollJobLogTop');
+ },
+ handleScrollToBottom() {
+ this.$emit('scrollJobLogBottom');
+ },
+ },
+ };
+</script>
+<template>
+ <div class="top-bar">
+ <!-- truncate information -->
+ <div class="js-truncated-info truncated-info d-none d-sm-block float-left">
+ <p v-html="jobLogSize"></p>
+
+ <a
+ v-if="rawTracePath"
+ :href="rawTracePath"
+ class="js-raw-link raw-link"
+ >
+ {{ s__("Job|Complete Raw") }}
+ </a>
+ </div>
+ <!-- eo truncate information -->
+
+ <div class="controllers float-right">
+ <!-- links -->
+ <a
+ v-tooltip
+ v-if="rawTracePath"
+ :title="s__('Job|Show complete raw')"
+ :href="rawTracePath"
+ class="js-raw-link-controller controllers-buttons"
+ data-container="body"
+ >
+ <icon name="doc-text" />
+ </a>
+
+ <button
+ v-tooltip
+ v-if="canEraseJob"
+ :title="s__('Job|Erase job log')"
+ type="button"
+ class="js-erase-link controllers-buttons"
+ data-container="body"
+ @click="handleEraseJobClick"
+ >
+ <icon name="remove" />
+ </button>
+ <!-- eo links -->
+
+ <!-- scroll buttons -->
+ <div
+ v-tooltip
+ :title="s__('Job|Scroll to top')"
+ class="controllers-buttons"
+ data-container="body"
+ >
+ <button
+ :disabled="!canScrollToTop"
+ type="button"
+ class="js-scroll-top btn-scroll btn-transparent btn-blank"
+ @click="handleScrollToTop"
+ >
+ <icon name="scroll_up"/>
+ </button>
+ </div>
+
+ <div
+ v-tooltip
+ :title="s__('Job|Scroll to bottom')"
+ class="controllers-buttons"
+ data-container="body"
+ >
+ <button
+ :disabled="!canScrollToBottom"
+ type="button"
+ class="js-scroll-bottom btn-scroll btn-transparent btn-blank"
+ @click="handleScrollToBottom"
+ >
+ <icon name="scroll_down"/>
+ </button>
+ </div>
+ <!-- eo scroll buttons -->
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/jobs_container.vue b/app/assets/javascripts/jobs/components/jobs_container.vue
new file mode 100644
index 00000000000..b81109bdd06
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/jobs_container.vue
@@ -0,0 +1,60 @@
+<script>
+ import CiIcon from '~/vue_shared/components/ci_icon.vue';
+ import Icon from '~/vue_shared/components/icon.vue';
+ import tooltip from '~/vue_shared/directives/tooltip';
+
+ export default {
+ components: {
+ CiIcon,
+ Icon,
+ },
+ directives: {
+ tooltip,
+ },
+ props: {
+ jobs: {
+ type: Array,
+ required: true,
+ },
+ },
+ };
+</script>
+<template>
+ <div class="builds-container">
+ <div
+ class="build-job"
+ >
+ <a
+ v-tooltip
+ v-for="job in jobs"
+ :key="job.id"
+ :href="job.path"
+ :title="job.tooltip"
+ :class="{ active: job.active, retried: job.retried }"
+ >
+ <icon
+ v-if="job.active"
+ name="arrow-right"
+ class="js-arrow-right"
+ />
+
+ <ci-icon :status="job.status" />
+
+ <span>
+ <template v-if="job.name">
+ {{ job.name }}
+ </template>
+ <template v-else>
+ {{ job.id }}
+ </template>
+ </span>
+
+ <icon
+ v-if="job.retried"
+ name="retry"
+ class="js-retry-icon"
+ />
+ </a>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/stages_dropdown.vue b/app/assets/javascripts/jobs/components/stages_dropdown.vue
new file mode 100644
index 00000000000..d6d64fa32f7
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/stages_dropdown.vue
@@ -0,0 +1,97 @@
+<script>
+ import CiIcon from '~/vue_shared/components/ci_icon.vue';
+ import Icon from '~/vue_shared/components/icon.vue';
+
+ import { sprintf, __ } from '~/locale';
+
+ export default {
+ components: {
+ CiIcon,
+ Icon,
+ },
+ props: {
+ pipelineId: {
+ type: Number,
+ required: true,
+ },
+ pipelinePath: {
+ type: String,
+ required: true,
+ },
+ pipelineRef: {
+ type: String,
+ required: true,
+ },
+ pipelineRefPath: {
+ type: String,
+ required: true,
+ },
+ stages: {
+ type: Array,
+ required: true,
+ },
+ pipelineStatus: {
+ type: Object,
+ required: true,
+ },
+ },
+ data() {
+ return {
+ selectedStage: this.stages.length > 0 ? this.stages[0].name : __('More'),
+ };
+ },
+ computed: {
+ pipelineLink() {
+ return sprintf(__('Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}'), {
+ pipelineLinkStart: `<a href=${this.pipelinePath} class="js-pipeline-path link-commit">`,
+ pipelineId: this.pipelineId,
+ pipelineLinkEnd: '</a>',
+ pipelineLinkRefStart: `<a href=${this.pipelineRefPath} class="link-commit ref-name">`,
+ pipelineRef: this.pipelineRef,
+ pipelineLinkRefEnd: '</a>',
+ }, false);
+ },
+ },
+ methods: {
+ onStageClick(stage) {
+ // todo: consider moving into store
+ this.selectedStage = stage.name;
+
+ // update dropdown with jobs
+ // jobs container is a new component.
+ this.$emit('requestSidebarStageDropdown', stage);
+ },
+ },
+ };
+</script>
+<template>
+ <div class="block-last">
+ <ci-icon :status="pipelineStatus" />
+
+ <p v-html="pipelineLink"></p>
+
+ <div class="dropdown">
+ <button
+ type="button"
+ data-toggle="dropdown"
+ >
+ {{ selectedStage }}
+ <icon name="chevron-down" />
+ </button>
+ <ul class="dropdown-menu">
+ <li
+ v-for="(stage, index) in stages"
+ :key="index"
+ >
+ <button
+ type="button"
+ class="stage-item"
+ @click="onStageClick(stage)"
+ >
+ {{ stage.name }}
+ </button>
+ </li>
+ </ul>
+ </div>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/trigger_block.vue b/app/assets/javascripts/jobs/components/trigger_block.vue
new file mode 100644
index 00000000000..8a88e5da6aa
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/trigger_block.vue
@@ -0,0 +1,84 @@
+<script>
+ export default {
+ props: {
+ shortToken: {
+ type: String,
+ required: false,
+ default: null,
+ },
+
+ variables: {
+ type: Object,
+ required: false,
+ default: () => ({}),
+ },
+ },
+ data() {
+ return {
+ areVariablesVisible: false,
+ };
+ },
+ computed: {
+ hasVariables() {
+ return Object.keys(this.variables).length > 0;
+ },
+ },
+ methods: {
+ revealVariables() {
+ this.areVariablesVisible = true;
+ },
+ },
+ };
+</script>
+
+<template>
+ <div class="build-widget block">
+ <h4 class="title">
+ {{ __('Trigger') }}
+ </h4>
+
+ <p
+ v-if="shortToken"
+ class="js-short-token"
+ >
+ <span class="build-light-text">
+ {{ __('Token') }}
+ </span>
+ {{ shortToken }}
+ </p>
+
+ <p v-if="hasVariables">
+ <button
+ type="button"
+ class="btn btn-default group js-reveal-variables"
+ @click="revealVariables"
+ >
+ {{ __('Reveal Variables') }}
+ </button>
+
+ </p>
+
+ <dl
+ v-if="areVariablesVisible"
+ class="js-build-variables trigger-build-variables"
+ >
+ <template
+ v-for="(value, key) in variables"
+ >
+ <dt
+ :key="`${key}-variable`"
+ class="js-build-variable trigger-build-variable"
+ >
+ {{ key }}
+ </dt>
+
+ <dd
+ :key="`${key}-value`"
+ class="js-build-value trigger-build-value"
+ >
+ {{ value }}
+ </dd>
+ </template>
+ </dl>
+ </div>
+</template>
diff --git a/app/assets/javascripts/locale/ensure_single_line.js b/app/assets/javascripts/locale/ensure_single_line.js
new file mode 100644
index 00000000000..47c52fe6c50
--- /dev/null
+++ b/app/assets/javascripts/locale/ensure_single_line.js
@@ -0,0 +1,25 @@
+/* eslint-disable import/no-commonjs */
+
+const SPLIT_REGEX = /\s*[\r\n]+\s*/;
+
+/**
+ *
+ * strips newlines from strings and replaces them with a single space
+ *
+ * @example
+ *
+ * ensureSingleLine('foo \n bar') === 'foo bar'
+ *
+ * @param {String} str
+ * @returns {String}
+ */
+module.exports = function ensureSingleLine(str) {
+ // This guard makes the function significantly faster
+ if (str.includes('\n') || str.includes('\r')) {
+ return str
+ .split(SPLIT_REGEX)
+ .filter(s => s !== '')
+ .join(' ');
+ }
+ return str;
+};
diff --git a/app/assets/javascripts/locale/index.js b/app/assets/javascripts/locale/index.js
index 2cc5fb10027..1ae3362c4bc 100644
--- a/app/assets/javascripts/locale/index.js
+++ b/app/assets/javascripts/locale/index.js
@@ -1,4 +1,5 @@
import Jed from 'jed';
+import ensureSingleLine from './ensure_single_line';
import sprintf from './sprintf';
const languageCode = () => document.querySelector('html').getAttribute('lang') || 'en';
@@ -10,7 +11,7 @@ delete window.translations;
@param text The text to be translated
@returns {String} The translated text
*/
-const gettext = locale.gettext.bind(locale);
+const gettext = text => locale.gettext.bind(locale)(ensureSingleLine(text));
/**
Translate the text with a number
@@ -23,7 +24,10 @@ const gettext = locale.gettext.bind(locale);
@returns {String} Translated text with the number replaced (eg. '2 days')
*/
const ngettext = (text, pluralText, count) => {
- const translated = locale.ngettext(text, pluralText, count).replace(/%d/g, count).split('|');
+ const translated = locale
+ .ngettext(ensureSingleLine(text), ensureSingleLine(pluralText), count)
+ .replace(/%d/g, count)
+ .split('|');
return translated[translated.length - 1];
};
@@ -40,7 +44,7 @@ const ngettext = (text, pluralText, count) => {
@returns {String} Translated context based text
*/
const pgettext = (keyOrContext, key) => {
- const normalizedKey = key ? `${keyOrContext}|${key}` : keyOrContext;
+ const normalizedKey = ensureSingleLine(key ? `${keyOrContext}|${key}` : keyOrContext);
const translated = gettext(normalizedKey).split('|');
return translated[translated.length - 1];
@@ -52,8 +56,7 @@ const pgettext = (keyOrContext, key) => {
@param formatOptions for available options, please see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/DateTimeFormat
@returns {Intl.DateTimeFormat}
*/
-const createDateTimeFormat =
- formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
+const createDateTimeFormat = formatOptions => Intl.DateTimeFormat(languageCode(), formatOptions);
export { languageCode };
export { gettext as __ };
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
index 2d5020dbef4..2d5020dbef4 100644
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
index 914a9661c27..914a9661c27 100644
--- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js
+++ b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
index c1056537f90..c1056537f90 100644
--- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 5e7b8f9698f..63082654101 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import $ from 'jquery';
import eventHub from '../../event_hub';
@@ -17,7 +18,7 @@ export default {
computed: {
buttonText() {
- return this.isLocked ? this.__('Unlock') : this.__('Lock');
+ return this.isLocked ? __('Unlock') : __('Lock');
},
toggleLock() {
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 8bbc59f623a..ab7fab7e5ca 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,5 +1,5 @@
<script>
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
@@ -79,11 +79,9 @@ export default {
.then(() => window.location.reload())
.catch(() =>
Flash(
- this.__(
- `Something went wrong trying to change the locked state of this ${
- this.issuableDisplayName
- }`,
- ),
+ sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), {
+ issuableDisplayName: this.issuableDisplayName,
+ }),
),
);
},
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 8d11b92cf88..a265e4206f1 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -141,8 +141,8 @@
&:hover,
&:active,
&.is-active {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
box-shadow: none;
outline: 0;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 48a87ea8616..79ca6e61e9a 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -71,7 +71,7 @@ pre {
}
&.card.card-body-pre {
- border: 1px solid $well-pre-bg;
+ border: 1px solid $gray-darker;
background: $gray-light;
border-radius: 0;
color: $well-pre-color;
@@ -233,7 +233,7 @@ li.note {
.error-message {
padding: 10px;
- background: $error-bg;
+ background: $red-400;
margin: 0;
color: $white-light;
@@ -244,11 +244,11 @@ li.note {
}
.warning_message {
- border-left: 4px solid $warning-message-border;
- color: $warning-message-color;
+ border-left: 4px solid $orange-200;
+ color: $orange-700;
padding: 10px;
margin-bottom: 10px;
- background: $warning-message-bg;
+ background: $orange-100;
padding-left: 20px;
&.centered {
@@ -348,15 +348,6 @@ img.emoji {
}
}
-.profiler-results {
- top: 73px !important;
-
- .profiler-button,
- .profiler-controls {
- border-color: $profiler-border !important;
- }
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 54882633fea..3bdf5bfc93a 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -112,7 +112,7 @@
&.image_file,
&.video {
- background: $file-image-bg;
+ background: $gray-darker;
text-align: center;
padding: 30px;
@@ -131,7 +131,7 @@
}
&.blob-no-preview {
- background: $blob-bg;
+ background: $gray-darker;
text-shadow: 0 1px 2px $white-light;
padding: 100px 0;
}
@@ -146,7 +146,7 @@
}
tr {
- border-bottom: 1px solid $blame-border;
+ border-bottom: 1px solid $gray-darker;
&:last-child {
border-bottom: 0;
@@ -211,7 +211,7 @@
}
&.logs {
- background: $logs-bg;
+ background: $gray-darker;
max-height: 700px;
overflow-y: auto;
@@ -233,7 +233,7 @@
}
&:hover {
- background: $row-hover;
+ background: $blue-50;
}
}
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 86de88729ee..da5f80d9d37 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -39,7 +39,7 @@
}
&.status-box-expired {
- background-color: $issue-status-expired;
+ background-color: $orange-500;
}
&.status-box-upcoming {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 4b67eab05b3..fdc0454d837 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -21,11 +21,11 @@
}
&.disabled {
- color: $list-text-disabled-color;
+ color: $gl-text-color-tertiary;
}
&:not(.ui-sort-disabled):hover {
- background: $row-hover;
+ background: $blue-50;
}
&.unstyled {
@@ -35,12 +35,12 @@
}
&.warning-row {
- background-color: $list-warning-row-bg;
- border-color: $list-warning-row-border;
- color: $list-warning-row-color;
+ background-color: $orange-100;
+ border-color: $orange-200;
+ color: $orange-700;
&:hover {
- background: $list-warning-row-bg;
+ background: $orange-100;
}
}
@@ -73,7 +73,7 @@
}
.card.card-body-title {
- font-size: $list-font-size;
+ font-size: $gl-font-size;
line-height: 18px;
}
}
@@ -109,8 +109,8 @@ ul.content-list {
li {
border-color: $white-normal;
- font-size: $list-font-size;
- color: $list-text-color;
+ font-size: $gl-font-size;
+ color: $gl-text-color;
&.no-description {
.title {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 98bf26a5222..7edb89ce6f3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -50,7 +50,7 @@
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid $list-border-light;
+ border-bottom: 1px solid $gray-darker;
display: block;
margin: 0;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index eccc814b747..5c6110737a4 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -7,7 +7,7 @@
}
a {
- color: $md-link-color;
+ color: $blue-600;
}
img:not(.emoji) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 866cb88ba5b..96508a71bd8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -203,21 +203,12 @@ $gl-header-color: #4c4e54;
/*
* Lists
*/
-$list-font-size: $gl-font-size;
-$list-title-color: $gl-text-color;
-$list-text-color: $gl-text-color;
-$list-text-disabled-color: $gl-text-color-tertiary;
-$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
-$list-warning-row-bg: $orange-100;
-$list-warning-row-border: $orange-200;
-$list-warning-row-color: $orange-700;
/*
* Markdown
*/
-$md-link-color: $blue-600;
$md-area-border: #ddd;
/*
@@ -249,8 +240,6 @@ $gl-bar-padding: 3px;
/*
* Misc
*/
-$row-hover: $blue-50;
-$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
$ide-statusbar-height: 25px;
@@ -258,19 +247,13 @@ $fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
-$error-exclamation-point: $red-500;
$border-radius-default: 4px;
$border-radius-small: 2px;
$settings-icon-size: 18px;
-$provider-btn-not-active-color: $blue-500;
-$link-underline-blue: $blue-500;
-$active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-margin-5: 5px;
-$issue-status-expired: $orange-500;
-$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
@@ -292,7 +275,6 @@ $breadcrumb-min-height: 48px;
* Common component specific colors
*/
$hint-color: #999;
-$well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
@@ -301,10 +283,6 @@ $user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
-$error-bg: $red-400;
-$warning-message-bg: $orange-100;
-$warning-message-border: $orange-200;
-$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
$username-color: #666;
@@ -374,7 +352,7 @@ $dropdown-min-height: 40px;
$dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
-$dropdown-link-hover-bg: $row-hover;
+$dropdown-link-hover-bg: $blue-50;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, 0.1);
@@ -592,11 +570,7 @@ $events-body-border: #ddd;
/*
* Files
*/
-$file-image-bg: #eee;
-$blob-bg: #eee;
-$blame-border: #eee;
$blame-line-numbers-border: #ddd;
-$logs-bg: #eee;
$logs-li-color: #888;
$logs-p-color: #333;
@@ -667,7 +641,6 @@ $login-devise-error-color: $red-700;
* Nav
*/
$nav-link-gray: #959494;
-$nav-badge-bg: #eee;
$nav-toggle-gray: #666;
/*
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 91f470ca709..69d7de886b4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -288,7 +288,7 @@
&.is-active,
&.is-active .board-card-assignee:hover a {
- background-color: $row-hover;
+ background-color: $blue-50;
}
.badge {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index bba9f38d3dd..bc4c90711d7 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -165,7 +165,7 @@
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
- box-shadow: inset 2px 0 0 0 $active-item-blue;
+ box-shadow: inset 2px 0 0 0 $blue-500;
.stage-name {
font-weight: $gl-font-weight-bold;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 47778110bae..a999a70693e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -186,7 +186,7 @@
}
.image {
- background: $file-image-bg;
+ background: $gray-darker;
text-align: center;
padding: 30px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index f79586b68b9..da0c9b44498 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -6,7 +6,7 @@
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top 40px;
border-bottom: 1px solid $white-normal;
- color: $list-text-color;
+ color: $gl-text-color;
position: relative;
&.event-inline {
@@ -58,7 +58,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
font-weight: $gl-font-weight-bold;
- color: $list-text-color;
+ color: $gl-text-color;
}
.event-body {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index fa8a0f26b5d..60b4d39bb1a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -385,8 +385,8 @@
padding: $gl-padding-top;
&:hover {
- border-color: $row-hover-border;
- background-color: $row-hover;
+ border-color: $blue-200;
+ background-color: $blue-50;
cursor: pointer;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d16a63d009a..6f0f82964c8 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -423,10 +423,10 @@
width: 100%;
text-align: center;
margin-bottom: 10px;
- color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
svg {
- fill: $issuable-sidebar-color;
+ fill: $gl-text-color-secondary;
}
&:hover:not(.disabled),
@@ -457,14 +457,14 @@
}
.avatar-counter:hover {
- color: $issuable-sidebar-color;
- border-color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
+ border-color: $gl-text-color-secondary;
}
.btn-clipboard {
border: 0;
background: transparent;
- color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
&:hover {
color: $gl-text-color;
@@ -821,7 +821,7 @@
svg {
width: 16px;
height: 16px;
- fill: $issuable-sidebar-color;
+ fill: $gl-text-color-secondary;
}
&:hover svg {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8d28daac750..2e1b2126887 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -141,9 +141,6 @@ ul.notes {
}
.note-body {
- overflow-x: auto;
- overflow-y: hidden;
-
.note-text {
@include md-typography;
// Reset ul style types since we're nested inside a ul already
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b45e305897c..17f34319050 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -20,7 +20,7 @@
.account-btn-link,
.profile-settings-sidebar a,
.settings-sidebar a {
- color: $md-link-color;
+ color: $blue-600;
}
.private-tokens-reset div.reset-action:not(:first-child) {
@@ -137,7 +137,7 @@
.profile-settings-content {
a {
- color: $md-link-color;
+ color: $blue-600;
}
}
@@ -170,7 +170,7 @@
background-color: $gray-light;
&.not-active {
- color: $provider-btn-not-active-color;
+ color: $blue-500;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index fffb440027c..c11916454c8 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -449,8 +449,8 @@
&:hover:not(.disabled),
&.forked {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
}
.avatar-container,
@@ -894,7 +894,7 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: $error-exclamation-point;
+ color: $red-500;
margin-top: 2px;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index fb03970f64f..5aa4cdec9c3 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -113,9 +113,9 @@
.settings-message {
padding: 5px;
line-height: 1.3;
- color: $warning-message-color;
- background-color: $warning-message-bg;
- border: 1px solid $warning-message-border;
+ color: $orange-700;
+ background-color: $orange-100;
+ border: 1px solid $orange-200;
border-radius: $border-radius-base;
}
@@ -303,7 +303,7 @@
}
.mirror-error-badge {
- background-color: $error-bg;
+ background-color: $red-400;
border-radius: $border-radius-default;
color: $white-light;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 010a2c05a1c..5d3b7b21ce4 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -12,8 +12,8 @@
flex-direction: row;
&:hover {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
cursor: pointer;
}
@@ -22,7 +22,7 @@
border-bottom: 1px solid transparent;
&:hover {
- border-color: $row-hover-border;
+ border-color: $blue-200;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1cc26d40ba9..dc5ca78ff58 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -111,9 +111,9 @@
&:hover:not(.tree-truncated-warning) {
td {
- background-color: $row-hover;
- border-top: 1px solid $row-hover-border;
- border-bottom: 1px solid $row-hover-border;
+ background-color: $blue-50;
+ border-top: 1px solid $blue-200;
+ border-bottom: 1px solid $blue-200;
cursor: pointer;
}
}
@@ -229,7 +229,7 @@
.upload-link {
font-weight: $gl-font-weight-normal;
- color: $md-link-color;
+ color: $blue-600;
}
.repo-charts {
diff --git a/app/controllers/projects/pages_controller.rb b/app/controllers/projects/pages_controller.rb
index cae6e2c40b8..ff49911d892 100644
--- a/app/controllers/projects/pages_controller.rb
+++ b/app/controllers/projects/pages_controller.rb
@@ -11,7 +11,7 @@ class Projects::PagesController < Projects::ApplicationController
def destroy
project.remove_pages
- project.pages_domains.destroy_all
+ project.pages_domains.destroy_all # rubocop: disable DestroyAll
respond_to do |format|
format.html do
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 3c69677baf0..faa160ad6ba 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -67,6 +67,10 @@ module Ci
'', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').archive)
end
+ scope :with_archived_trace, ->() do
+ where('EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
+ end
+
scope :without_archived_trace, ->() do
where('NOT EXISTS (?)', Ci::JobArtifact.select(1).where('ci_builds.id = ci_job_artifacts.job_id').trace)
end
@@ -77,6 +81,7 @@ module Ci
end
scope :with_artifacts_stored_locally, -> { with_artifacts_archive.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
+ scope :with_archived_trace_stored_locally, -> { with_archived_trace.where(artifacts_file_store: [nil, LegacyArtifactUploader::Store::LOCAL]) }
scope :with_artifacts_not_expired, ->() { with_artifacts_archive.where('artifacts_expire_at IS NULL OR artifacts_expire_at > ?', Time.now) }
scope :with_expired_artifacts, ->() { with_artifacts_archive.where('artifacts_expire_at < ?', Time.now) }
scope :last_month, ->() { where('created_at > ?', Date.today - 1.month) }
diff --git a/app/models/concerns/awardable.rb b/app/models/concerns/awardable.rb
index dd07f389fa5..49981db0d80 100644
--- a/app/models/concerns/awardable.rb
+++ b/app/models/concerns/awardable.rb
@@ -101,7 +101,7 @@ module Awardable
end
def remove_award_emoji(name, current_user)
- award_emoji.where(name: name, user: current_user).destroy_all
+ award_emoji.where(name: name, user: current_user).destroy_all # rubocop: disable DestroyAll
end
def toggle_award_emoji(emoji_name, current_user)
diff --git a/app/models/concerns/fast_destroy_all.rb b/app/models/concerns/fast_destroy_all.rb
index 65ed46ea202..c342d01243e 100644
--- a/app/models/concerns/fast_destroy_all.rb
+++ b/app/models/concerns/fast_destroy_all.rb
@@ -34,7 +34,7 @@ module FastDestroyAll
included do
before_destroy do
- raise ForbiddenActionError, '`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`'
+ raise ForbiddenActionError, '`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`'
end
end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 4eb211eff61..e7168d49db9 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -111,7 +111,7 @@ class InternalId < ActiveRecord::Base
# Generates next internal id and returns it
def generate
- subject.transaction do
+ InternalId.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
#
@@ -125,7 +125,7 @@ class InternalId < ActiveRecord::Base
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
- subject.transaction do
+ InternalId.transaction do
(lookup || create_record).track_greatest_and_save!(new_value)
end
end
@@ -148,7 +148,7 @@ class InternalId < ActiveRecord::Base
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
def create_record
- subject.transaction(requires_new: true) do
+ InternalId.transaction(requires_new: true) do
InternalId.create!(
**scope,
usage: usage_value,
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 2a1a4ef48b7..97bf5d611c2 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -29,11 +29,13 @@ class LfsObject < ActiveRecord::Base
[nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
end
+ # rubocop: disable DestroyAll
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
.destroy_all
end
+ # rubocop: enable DestroyAll
def self.calculate_oid(path)
Digest::SHA256.file(path).hexdigest
diff --git a/app/models/project.rb b/app/models/project.rb
index 7735f23cb9e..94c1d60f071 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -27,6 +27,7 @@ class Project < ActiveRecord::Base
include FastDestroyAll::Helpers
include WithUploads
include BatchDestroyDependentAssociations
+ include FeatureGate
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -519,18 +520,19 @@ class Project < ActiveRecord::Base
def auto_devops_enabled?
if auto_devops&.enabled.nil?
- Gitlab::CurrentSettings.auto_devops_enabled?
+ has_auto_devops_implicitly_enabled?
else
auto_devops.enabled?
end
end
def has_auto_devops_implicitly_enabled?
- auto_devops&.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?
+ auto_devops&.enabled.nil? &&
+ (Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
end
def has_auto_devops_implicitly_disabled?
- auto_devops&.enabled.nil? && !Gitlab::CurrentSettings.auto_devops_enabled?
+ auto_devops&.enabled.nil? && !(Gitlab::CurrentSettings.auto_devops_enabled? || Feature.enabled?(:force_autodevops_on_by_default, self))
end
def empty_repo?
diff --git a/app/models/project_auto_devops.rb b/app/models/project_auto_devops.rb
index 155400d1a43..dc6736dd9cd 100644
--- a/app/models/project_auto_devops.rb
+++ b/app/models/project_auto_devops.rb
@@ -47,12 +47,8 @@ class ProjectAutoDevops < ActiveRecord::Base
end
def needs_to_create_deploy_token?
- auto_devops_enabled? &&
+ project.auto_devops_enabled? &&
!project.public? &&
!project.deploy_tokens.find_by(name: DeployToken::GITLAB_DEPLOY_TOKEN_NAME).present?
end
-
- def auto_devops_enabled?
- Gitlab::CurrentSettings.auto_devops_enabled? || enabled?
- end
end
diff --git a/app/models/site_statistic.rb b/app/models/site_statistic.rb
index daac1c57db9..48324570f0b 100644
--- a/app/models/site_statistic.rb
+++ b/app/models/site_statistic.rb
@@ -49,7 +49,7 @@ class SiteStatistic < ActiveRecord::Base
#
# @return [SiteStatistic] record with tracked information
def self.fetch
- SiteStatistic.transaction(requires_new: true) do
+ transaction(requires_new: true) do
SiteStatistic.first_or_create!
end
rescue ActiveRecord::RecordNotUnique
diff --git a/app/models/user.rb b/app/models/user.rb
index fb19de4b980..13b04270a4a 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -516,7 +516,7 @@ class User < ActiveRecord::Base
otp_grace_period_started_at: nil,
otp_backup_codes: nil
)
- self.u2f_registrations.destroy_all
+ self.u2f_registrations.destroy_all # rubocop: disable DestroyAll
end
end
diff --git a/app/services/groups/destroy_service.rb b/app/services/groups/destroy_service.rb
index c4554ce45fb..12aeba4af71 100644
--- a/app/services/groups/destroy_service.rb
+++ b/app/services/groups/destroy_service.rb
@@ -2,6 +2,8 @@
module Groups
class DestroyService < Groups::BaseService
+ DestroyError = Class.new(StandardError)
+
def async_execute
job_id = GroupDestroyWorker.perform_async(group.id, current_user.id)
Rails.logger.info("User #{current_user.id} scheduled a deletion of group ID #{group.id} with job ID #{job_id}")
@@ -12,9 +14,8 @@ module Groups
group.projects.each do |project|
# Execute the destruction of the models immediately to ensure atomic cleanup.
- # Skip repository removal because we remove directory with namespace
- # that contain all these repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
+ success = ::Projects::DestroyService.new(project, current_user).execute
+ raise DestroyError, "Project #{project.id} can't be deleted" unless success
end
group.children.each do |group|
diff --git a/app/services/labels/promote_service.rb b/app/services/labels/promote_service.rb
index c0463052821..623a5f0950e 100644
--- a/app/services/labels/promote_service.rb
+++ b/app/services/labels/promote_service.rb
@@ -65,7 +65,7 @@ module Labels
end
def update_project_labels(label_ids)
- Label.where(id: label_ids).destroy_all
+ Label.where(id: label_ids).destroy_all # rubocop: disable DestroyAll
end
def clone_label_to_group_label(label)
diff --git a/app/services/milestones/promote_service.rb b/app/services/milestones/promote_service.rb
index 37aa6d3a9bc..660b4faaec0 100644
--- a/app/services/milestones/promote_service.rb
+++ b/app/services/milestones/promote_service.rb
@@ -73,7 +73,7 @@ module Milestones
end
def destroy_old_milestones(milestone)
- Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all
+ Milestone.where(id: milestone_ids_for_merge(milestone)).destroy_all # rubocop: disable DestroyAll
end
def group_project_ids
diff --git a/app/services/projects/destroy_service.rb b/app/services/projects/destroy_service.rb
index 46a8a5e4d98..76e22507698 100644
--- a/app/services/projects/destroy_service.rb
+++ b/app/services/projects/destroy_service.rb
@@ -83,9 +83,6 @@ module Projects
end
def remove_repository(path)
- # Skip repository removal. We use this flag when remove user or group
- return true if params[:skip_repo] == true
-
# There is a possibility project does not have repository or wiki
return true unless repo_exists?(path)
diff --git a/app/services/projects/move_deploy_keys_projects_service.rb b/app/services/projects/move_deploy_keys_projects_service.rb
index 40a22837eaf..9f3f44f30ea 100644
--- a/app/services/projects/move_deploy_keys_projects_service.rb
+++ b/app/services/projects/move_deploy_keys_projects_service.rb
@@ -27,7 +27,7 @@ module Projects
end
def remove_remaining_deploy_keys_projects
- source_project.deploy_keys_projects.destroy_all
+ source_project.deploy_keys_projects.destroy_all # rubocop: disable DestroyAll
end
end
end
diff --git a/app/services/projects/move_lfs_objects_projects_service.rb b/app/services/projects/move_lfs_objects_projects_service.rb
index a5099519594..f78546a1e9c 100644
--- a/app/services/projects/move_lfs_objects_projects_service.rb
+++ b/app/services/projects/move_lfs_objects_projects_service.rb
@@ -21,7 +21,7 @@ module Projects
end
def remove_remaining_lfs_objects_project
- source_project.lfs_objects_projects.destroy_all
+ source_project.lfs_objects_projects.destroy_all # rubocop: disable DestroyAll
end
def non_existent_lfs_objects_projects
diff --git a/app/services/projects/move_notification_settings_service.rb b/app/services/projects/move_notification_settings_service.rb
index 746605d56f1..109a00dd6d9 100644
--- a/app/services/projects/move_notification_settings_service.rb
+++ b/app/services/projects/move_notification_settings_service.rb
@@ -22,7 +22,7 @@ module Projects
# Remove remaining notification settings from source_project
def remove_remaining_notification_settings
- source_project.notification_settings.destroy_all
+ source_project.notification_settings.destroy_all # rubocop: disable DestroyAll
end
# Get users of current notification_settings
diff --git a/app/services/projects/move_project_group_links_service.rb b/app/services/projects/move_project_group_links_service.rb
index d9038030f7e..1efafdce36d 100644
--- a/app/services/projects/move_project_group_links_service.rb
+++ b/app/services/projects/move_project_group_links_service.rb
@@ -26,7 +26,7 @@ module Projects
# Remove remaining project group links from source_project
def remove_remaining_project_group_links
- source_project.reload.project_group_links.destroy_all
+ source_project.reload.project_group_links.destroy_all # rubocop: disable DestroyAll
end
def group_links_in_target_project
diff --git a/app/services/projects/move_project_members_service.rb b/app/services/projects/move_project_members_service.rb
index bb0c0d10242..ec983582d94 100644
--- a/app/services/projects/move_project_members_service.rb
+++ b/app/services/projects/move_project_members_service.rb
@@ -25,7 +25,7 @@ module Projects
def remove_remaining_members
# Remove remaining members and authorizations from source_project
- source_project.project_members.destroy_all
+ source_project.project_members.destroy_all # rubocop: disable DestroyAll
end
def project_members_in_target_project
diff --git a/app/services/projects/update_service.rb b/app/services/projects/update_service.rb
index 97f181ccea8..e390d7a04c3 100644
--- a/app/services/projects/update_service.rb
+++ b/app/services/projects/update_service.rb
@@ -30,7 +30,7 @@ module Projects
def run_auto_devops_pipeline?
return false if project.repository.gitlab_ci_yml || !project.auto_devops&.previous_changes&.include?('enabled')
- project.auto_devops.enabled? || (project.auto_devops.enabled.nil? && Gitlab::CurrentSettings.auto_devops_enabled?)
+ project.auto_devops_enabled?
end
private
diff --git a/app/services/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index 1f6bbe72f85..da8bf2ce02a 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -38,11 +38,11 @@ module ProtectedBranches
def delete_redundant_access_levels
unless @developers_can_merge.nil?
- @protected_branch.merge_access_levels.destroy_all
+ @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll
end
unless @developers_can_push.nil?
- @protected_branch.push_access_levels.destroy_all
+ @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll
end
end
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 4bc78b5b64e..73fa6089945 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -2,6 +2,8 @@
module Users
class DestroyService
+ DestroyError = Class.new(StandardError)
+
attr_accessor :current_user
def initialize(current_user)
@@ -46,9 +48,8 @@ module Users
namespace.prepare_for_destroy
user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
+ success = ::Projects::DestroyService.new(project, current_user).execute
+ raise DestroyError, "Project #{project.id} can't be deleted" unless success
end
yield(user) if block_given?
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 5037017e38a..97be658cd34 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -38,6 +38,8 @@
.form-text.text-muted
Set the default expiration time for each job's artifacts.
0 for unlimited.
+ The default unit is in seconds, but you can define an alternative. For example:
+ <code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/users/_access_levels.html.haml b/app/views/admin/users/_access_levels.html.haml
index 04acc5f8423..5f68163163e 100644
--- a/app/views/admin/users/_access_levels.html.haml
+++ b/app/views/admin/users/_access_levels.html.haml
@@ -1,15 +1,18 @@
%fieldset
%legend Access
.form-group.row
- = f.label :projects_limit, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :projects_limit, class: 'col-form-label'
.col-sm-10= f.number_field :projects_limit, min: 0, max: Gitlab::Database::MAX_INT_VALUE, class: 'form-control'
.form-group.row
- = f.label :can_create_group, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :can_create_group, class: 'col-form-label'
.col-sm-10= f.check_box :can_create_group
.form-group.row
- = f.label :access_level, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :access_level, class: 'col-form-label'
.col-sm-10
- editing_current_user = (current_user == @user)
@@ -29,7 +32,8 @@
You cannot remove your own admin rights.
.form-group.row
- = f.label :external, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :external, class: 'col-form-label'
.col-sm-10
= f.check_box :external do
External
diff --git a/app/views/admin/users/_form.html.haml b/app/views/admin/users/_form.html.haml
index 58be07fc83e..7f21bdb91c8 100644
--- a/app/views/admin/users/_form.html.haml
+++ b/app/views/admin/users/_form.html.haml
@@ -5,17 +5,20 @@
%fieldset
%legend Account
.form-group.row
- = f.label :name, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :name, class: 'col-form-label'
.col-sm-10
= f.text_field :name, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
.form-group.row
- = f.label :username, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :username, class: 'col-form-label'
.col-sm-10
= f.text_field :username, required: true, autocomplete: 'off', autocorrect: 'off', autocapitalize: 'off', spellcheck: false, class: 'form-control'
%span.help-inline * required
.form-group.row
- = f.label :email, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :email, class: 'col-form-label'
.col-sm-10
= f.text_field :email, required: true, autocomplete: 'off', class: 'form-control'
%span.help-inline * required
@@ -24,7 +27,8 @@
%fieldset
%legend Password
.form-group.row
- = f.label :password, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :password, class: 'col-form-label'
.col-sm-10
%strong
Reset link will be generated and sent to the user.
@@ -34,10 +38,12 @@
%fieldset
%legend Password
.form-group.row
- = f.label :password, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :password, class: 'col-form-label'
.col-sm-10= f.password_field :password, disabled: f.object.force_random_password, class: 'form-control'
.form-group.row
- = f.label :password_confirmation, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :password_confirmation, class: 'col-form-label'
.col-sm-10= f.password_field :password_confirmation, disabled: f.object.force_random_password, class: 'form-control'
= render partial: 'access_levels', locals: { f: f }
@@ -45,21 +51,26 @@
%fieldset
%legend Profile
.form-group.row
- = f.label :avatar, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :avatar, class: 'col-form-label'
.col-sm-10
= f.file_field :avatar
.form-group.row
- = f.label :skype, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :skype, class: 'col-form-label'
.col-sm-10= f.text_field :skype, class: 'form-control'
.form-group.row
- = f.label :linkedin, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :linkedin, class: 'col-form-label'
.col-sm-10= f.text_field :linkedin, class: 'form-control'
.form-group.row
- = f.label :twitter, class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :twitter, class: 'col-form-label'
.col-sm-10= f.text_field :twitter, class: 'form-control'
.form-group.row
- = f.label :website_url, 'Website', class: 'col-form-label col-sm-2'
+ .col-sm-2.text-right
+ = f.label :website_url, 'Website', class: 'col-form-label'
.col-sm-10= f.text_field :website_url, class: 'form-control'
.form-actions
diff --git a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
index 701a4e62b39..6a7c999bff3 100644
--- a/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
+++ b/app/views/instance_statistics/cohorts/_cohorts_table.html.haml
@@ -3,7 +3,7 @@
User cohorts are shown for the last #{@cohorts[:months_included]}
months. Only users with activity are counted in the cohort total; inactive
users are counted separately.
- = link_to icon('question-circle'), help_page_path('user/admin_area/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
+ = link_to icon('question-circle'), help_page_path('user/instance_statistics/user_cohorts', anchor: 'cohorts'), title: 'About this feature', target: '_blank'
.table-holder
%table.table
diff --git a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
index d69c46194b4..dd795aee135 100644
--- a/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/_no_data.html.haml
@@ -4,4 +4,4 @@
%h4 Data is still calculating...
%p
In order to gather accurate feature usage data, it can take 1 to 2 weeks to see your index.
- = link_to 'Learn more', help_page_path('user/admin_area/monitoring/convdev'), target: '_blank'
+ = link_to 'Learn more', help_page_path('user/instance_statistics/convdev'), target: '_blank'
diff --git a/app/views/instance_statistics/conversational_development_index/index.html.haml b/app/views/instance_statistics/conversational_development_index/index.html.haml
index e3d1aa31dc2..dd63b98376f 100644
--- a/app/views/instance_statistics/conversational_development_index/index.html.haml
+++ b/app/views/instance_statistics/conversational_development_index/index.html.haml
@@ -19,7 +19,7 @@
index
%br
score
- = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/admin_area/monitoring/convdev')
+ = link_to icon('question-circle', 'aria-hidden' => 'true'), help_page_path('user/instance_statistics/convdev')
.convdev-cards.board-card-container
- @metric.cards.each do |card|
diff --git a/app/views/projects/mirrors/_mirror_repos.html.haml b/app/views/projects/mirrors/_mirror_repos.html.haml
index 53387b3a50c..c6764c7607a 100644
--- a/app/views/projects/mirrors/_mirror_repos.html.haml
+++ b/app/views/projects/mirrors/_mirror_repos.html.haml
@@ -1,7 +1,7 @@
- expanded = Rails.env.test?
- protocols = Gitlab::UrlSanitizer::ALLOWED_SCHEMES.join('|')
-%section.settings.project-mirror-settings.js-mirror-settings.no-animate{ class: ('expanded' if expanded) }
+%section.settings.project-mirror-settings.js-mirror-settings.no-animate#js-push-remote-settings{ class: ('expanded' if expanded) }
.settings-header
%h4= _('Mirroring repositories')
%button.btn.js-settings-toggle
diff --git a/app/views/projects/show.html.haml b/app/views/projects/show.html.haml
index 8a5abb64515..df8a5742450 100644
--- a/app/views/projects/show.html.haml
+++ b/app/views/projects/show.html.haml
@@ -21,8 +21,7 @@
%nav.project-stats{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
= render 'stat_anchor_list', anchors: @project.statistics_anchors(show_auto_devops_callout: show_auto_devops_callout)
= render 'stat_anchor_list', anchors: @project.statistics_buttons(show_auto_devops_callout: show_auto_devops_callout)
- - if Feature.enabled?(:repository_languages, @project.namespace.becomes(Namespace))
- = repository_languages_bar(@project.repository_languages)
+ = repository_languages_bar(@project.repository_languages)
%div{ class: [container_class, ("limit-container-width" unless fluid_layout)] }
- if @project.archived?
diff --git a/app/workers/detect_repository_languages_worker.rb b/app/workers/detect_repository_languages_worker.rb
index cfbbbffc8a6..854b74b884a 100644
--- a/app/workers/detect_repository_languages_worker.rb
+++ b/app/workers/detect_repository_languages_worker.rb
@@ -16,8 +16,6 @@ class DetectRepositoryLanguagesWorker
user = User.find_by(id: user_id)
return unless project && user
- return if Feature.disabled?(:repository_languages, project.namespace)
-
try_obtain_lease do
::Projects::DetectRepositoryLanguagesService.new(project, user).execute
end
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 6b8b972a440..25128caf72f 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -5,6 +5,6 @@ class RemoveExpiredGroupLinksWorker
include CronjobQueue
def perform
- ProjectGroupLink.expired.destroy_all
+ ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll
end
end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
index 17140ac4450..0f486f8991d 100644
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -6,7 +6,9 @@ class RemoveOldWebHookLogsWorker
WEB_HOOK_LOG_LIFETIME = 2.days
+ # rubocop: disable DestroyAll
def perform
WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
end
+ # rubocop: enable DestroyAll
end
diff --git a/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml
new file mode 100644
index 00000000000..bc0150c6586
--- /dev/null
+++ b/changelogs/unreleased/41738-fix-sorting-issues-is-wrong-in-list-with-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: "Fix If-Check the result that a function was executed several times"
+merge_request: 20640
+author: Max Dicker
+type: fixed
diff --git a/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml
new file mode 100644
index 00000000000..00e1f6e638a
--- /dev/null
+++ b/changelogs/unreleased/49770-fixes-input-alignment-on-user-admin-form-with-errors.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes input alignment in user admin form with errors
+merge_request: 21108
+author: Jacopo Beschi @jacopo-beschi
+type: fixed
diff --git a/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml
new file mode 100644
index 00000000000..bb7633abdb1
--- /dev/null
+++ b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-group-deletion.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: Project deletion may not log audit events during group deletion'
+merge_request: 21162
+author:
+type: fixed
diff --git a/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml
new file mode 100644
index 00000000000..a8e3d590a4a
--- /dev/null
+++ b/changelogs/unreleased/49796-project-deletion-may-not-log-audit-events-during-user-deletion.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: Project deletion may not log audit events during user deletion'
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/50101-builds-dropdown.yml b/changelogs/unreleased/50101-builds-dropdown.yml
new file mode 100644
index 00000000000..9194b0e0d31
--- /dev/null
+++ b/changelogs/unreleased/50101-builds-dropdown.yml
@@ -0,0 +1,6 @@
+---
+title: Creates vue components for stage dropdowns and job list container for job log
+ view
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50101-commit-block.yml b/changelogs/unreleased/50101-commit-block.yml
new file mode 100644
index 00000000000..f6bad4c8154
--- /dev/null
+++ b/changelogs/unreleased/50101-commit-block.yml
@@ -0,0 +1,5 @@
+---
+title: Creates vue component for commit block in job log page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50101-trigger.yml b/changelogs/unreleased/50101-trigger.yml
new file mode 100644
index 00000000000..df4243afa63
--- /dev/null
+++ b/changelogs/unreleased/50101-trigger.yml
@@ -0,0 +1,5 @@
+---
+title: Creates Vue component for trigger variables block in job log page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50101-truncated-job-information.yml b/changelogs/unreleased/50101-truncated-job-information.yml
new file mode 100644
index 00000000000..b873b8b7bf6
--- /dev/null
+++ b/changelogs/unreleased/50101-truncated-job-information.yml
@@ -0,0 +1,5 @@
+---
+title: Creates vue component for job log top bar with controllers
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml
new file mode 100644
index 00000000000..50a3b9c9aff
--- /dev/null
+++ b/changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml
@@ -0,0 +1,5 @@
+---
+title: Fix issue stopping Instance Statistics javascript to be executed
+merge_request: 21211
+author:
+type: fixed
diff --git a/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml b/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml
new file mode 100644
index 00000000000..bfea57d79e0
--- /dev/null
+++ b/changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml
@@ -0,0 +1,5 @@
+---
+title: Add migration to cleanup internal_ids inconsistency.
+merge_request: 20926
+author:
+type: fixed
diff --git a/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml
new file mode 100644
index 00000000000..d963dc5bac3
--- /dev/null
+++ b/changelogs/unreleased/add-ci_archive_traces_cron_worker-to-gitlab-yml.yml
@@ -0,0 +1,5 @@
+---
+title: Add an example of the configuration of archive trace cron worker in gitlab.yml.example
+merge_request: 20583
+author:
+type: other
diff --git a/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml
new file mode 100644
index 00000000000..b82344e3c9c
--- /dev/null
+++ b/changelogs/unreleased/add-rake-command-to-migrate-locally-persisted-archived-traces.yml
@@ -0,0 +1,5 @@
+---
+title: Add rake command to migrate archived traces from local storage to object storage
+merge_request: 21193
+author:
+type: added
diff --git a/changelogs/unreleased/emoji-cutoff-1px.yml b/changelogs/unreleased/emoji-cutoff-1px.yml
new file mode 100644
index 00000000000..815d9c177e8
--- /dev/null
+++ b/changelogs/unreleased/emoji-cutoff-1px.yml
@@ -0,0 +1,5 @@
+---
+title: Fix 1px cutoff of emojis
+merge_request: 21180
+author: gfyoung
+type: fixed
diff --git a/changelogs/unreleased/rails5-verbose-query-logs.yml b/changelogs/unreleased/rails5-verbose-query-logs.yml
new file mode 100644
index 00000000000..7585e75d30b
--- /dev/null
+++ b/changelogs/unreleased/rails5-verbose-query-logs.yml
@@ -0,0 +1,5 @@
+---
+title: 'Rails5: Enable verbose query logs'
+merge_request: 21231
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/repopulate_site_statistics.yml b/changelogs/unreleased/repopulate_site_statistics.yml
new file mode 100644
index 00000000000..1961088061d
--- /dev/null
+++ b/changelogs/unreleased/repopulate_site_statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate NULL wiki_access_level to correct number so we count active wikis correctly
+merge_request: 21030
+author:
+type: changed
diff --git a/changelogs/unreleased/rouge_3-2-1.yml b/changelogs/unreleased/rouge_3-2-1.yml
new file mode 100644
index 00000000000..b281a4f0e95
--- /dev/null
+++ b/changelogs/unreleased/rouge_3-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer
+merge_request: 21263
+author:
+type: changed
diff --git a/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml
new file mode 100644
index 00000000000..0e748c3a346
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.117.1 for Rouge update
+merge_request: 21277
+author:
+type: security
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 561ff57c9fb..4847a82236b 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -261,6 +261,9 @@ production: &base
# once per hour you will have concurrent 'git fsck' jobs.
repository_check_worker:
cron: "20 * * * *"
+ # Archive live traces which have not been archived yet
+ ci_archive_traces_cron_worker:
+ cron: "17 * * * *"
# Send admin emails once a week
admin_email_worker:
cron: "0 0 * * 0"
diff --git a/config/initializers/active_record_verbose_query_logs.rb b/config/initializers/active_record_verbose_query_logs.rb
index 44f86fec7e0..1c5fbc8e830 100644
--- a/config/initializers/active_record_verbose_query_logs.rb
+++ b/config/initializers/active_record_verbose_query_logs.rb
@@ -47,7 +47,9 @@ module ActiveRecord
end
end
- unless Gitlab.rails5?
+ if Rails.version.start_with?("5.2")
+ raise "Remove this monkey patch: #{__FILE__}"
+ else
prepend(VerboseQueryLogs) unless Rails.env.production?
end
end
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index 49551319435..c1342f48ebd 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -1,5 +1,6 @@
require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
+require 'json'
VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/
@@ -36,6 +37,20 @@ module GettextI18nRailsJs
".vue"
].include? ::File.extname(file)
end
+
+ def collect_for(file)
+ gettext_messages_by_file[file] || []
+ end
+
+ private
+
+ def gettext_messages_by_file
+ @gettext_messages_by_file ||= JSON.parse(load_messages)
+ end
+
+ def load_messages
+ `node scripts/frontend/extract_gettext_all.js --all`
+ end
end
end
end
diff --git a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
index 668c22bb51c..8ebf1a5234d 100644
--- a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
+++ b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
@@ -16,6 +16,6 @@ class RemoveAwardEmojisWithNoUser < ActiveRecord::Migration
# disable_ddl_transaction!
def up
- AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all
+ AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all # rubocop: disable DestroyAll
end
end
diff --git a/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb
new file mode 100644
index 00000000000..3b9b95ec9ca
--- /dev/null
+++ b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+class DeleteInconsistentInternalIdRecords < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # This migration cleans up any inconsistent records in internal_ids.
+ #
+ # That is, it deletes records that track a `last_value` that is
+ # smaller than the maximum internal id (usually `iid`) found in
+ # the corresponding model records.
+
+ def up
+ disable_statement_timeout do
+ delete_internal_id_records('issues', 'project_id')
+ delete_internal_id_records('merge_requests', 'project_id', 'target_project_id')
+ delete_internal_id_records('deployments', 'project_id')
+ delete_internal_id_records('milestones', 'project_id')
+ delete_internal_id_records('milestones', 'namespace_id', 'group_id')
+ delete_internal_id_records('ci_pipelines', 'project_id')
+ end
+ end
+
+ class InternalId < ActiveRecord::Base
+ self.table_name = 'internal_ids'
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 }
+ end
+
+ private
+
+ def delete_internal_id_records(base_table, scope_column_name, base_scope_column_name = scope_column_name)
+ sql = <<~SQL
+ SELECT id FROM ( -- workaround for MySQL
+ SELECT internal_ids.id FROM (
+ SELECT #{base_scope_column_name} AS #{scope_column_name}, max(iid) as maximum_iid from #{base_table} GROUP BY #{scope_column_name}
+ ) maxima JOIN internal_ids USING (#{scope_column_name})
+ WHERE internal_ids.usage=#{InternalId.usages.fetch(base_table)} AND maxima.maximum_iid > internal_ids.last_value
+ ) internal_ids
+ SQL
+
+ InternalId.where("id IN (#{sql})").tap do |ids| # rubocop:disable GitlabSecurity/SqlInjection
+ say "Deleting internal_id records for #{base_table}: #{ids.pluck(:project_id, :last_value)}" unless ids.empty?
+ end.delete_all
+ end
+end
diff --git a/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
new file mode 100644
index 00000000000..0a0a33299e4
--- /dev/null
+++ b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class MigrateNullWikiAccessLevels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ ProjectFeature.where(wiki_access_level: nil).each_batch do |relation|
+ relation.update_all(wiki_access_level: 20)
+ end
+
+ # We need to re-count wikis as previous attempt was not considering the NULLs.
+ transaction do
+ execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+
+ execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
+ end
+ end
+
+ def down
+ # there is no way to rollback this change, there are no downsides in keeping migrated data.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1288a98745c..9dc122b54b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180808162000) do
+ActiveRecord::Schema.define(version: 20180809195358) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/administration/high_availability/nfs.md b/doc/administration/high_availability/nfs.md
index 387c3fb6a5b..cd2284f5f2a 100644
--- a/doc/administration/high_availability/nfs.md
+++ b/doc/administration/high_availability/nfs.md
@@ -55,14 +55,14 @@ Below is an example of an NFS mount point defined in `/etc/fstab` we use on
GitLab.com:
```
-10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nobootwait,lookupcache=positive 0 2
+10.1.1.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
```
Notice several options that you should consider using:
| Setting | Description |
| ------- | ----------- |
-| `nobootwait` | Don't halt boot process waiting for this mount to become available
+| `nofail` | Don't halt boot process waiting for this mount to become available
| `lookupcache=positive` | Tells the NFS client to honor `positive` cache results but invalidates any `negative` cache results. Negative cache results cause problems with Git. Specifically, a `git push` can fail to register uniformly across all NFS clients. The negative cache causes the clients to 'remember' that the files did not exist previously.
## A single NFS mount
diff --git a/doc/administration/index.md b/doc/administration/index.md
index 112d14652af..030a2f95e23 100644
--- a/doc/administration/index.md
+++ b/doc/administration/index.md
@@ -93,7 +93,6 @@ created in snippets, wikis, and repos.
- [Postfix for incoming email](reply_by_email_postfix_setup.md): Set up a
basic Postfix mail server with IMAP authentication on Ubuntu for incoming
emails.
-- [User Cohorts](../user/admin_area/user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
[reply by email]: reply_by_email.md
[issues by email]: ../user/project/issues/create_new_issue.md#new-issue-via-email
@@ -137,7 +136,6 @@ created in snippets, wikis, and repos.
- [Monitoring uptime](../user/admin_area/monitoring/health_check.md): Check the server status using the health check endpoint.
- [IP whitelist](monitoring/ip_whitelist.md): Monitor endpoints that provide health check information when probed.
- [Monitoring GitHub imports](monitoring/github_imports.md): GitLab's GitHub Importer displays Prometheus metrics to monitor the health and progress of the importer.
-- [Conversational Development (ConvDev) Index](../user/admin_area/monitoring/convdev.md): Provides an overview of your entire instance's feature usage.
### Performance Monitoring
diff --git a/doc/administration/job_traces.md b/doc/administration/job_traces.md
index 24d1a3fd151..6e2f67f61bc 100644
--- a/doc/administration/job_traces.md
+++ b/doc/administration/job_traces.md
@@ -3,10 +3,6 @@
Job traces are sent by GitLab Runner while it's processing a job. You can see
traces in job pages, pipelines, email notifications, etc.
-There isn't a way to automatically expire old job logs, but it's safe to remove
-them if they're taking up too much space. If you remove the logs manually, the
-job output in the UI will be empty.
-
## Data flow
In general, there are two states in job traces: "live trace" and "archived trace".
@@ -57,11 +53,55 @@ To change the location where the job logs will be stored, follow the steps below
## Uploading traces to object storage
-An archived trace is considered as a [job artifact](job_artifacts.md).
-Therefore, when you [set up an object storage](job_artifacts.md#object-storage-settings),
+Archived traces are considered as [job artifacts](job_artifacts.md).
+Therefore, when you [set up the object storage integration](job_artifacts.md#object-storage-settings),
job traces are automatically migrated to it along with the other job artifacts.
-See [Data flow](#data-flow) to learn about the process.
+See "Phase 4: uploading" in [Data flow](#data-flow) to learn about the process.
+
+## How to archive legacy job trace files
+
+Legacy job traces, which were created before GitLab 10.5, were not archived regularly.
+It's the same state with the "2: overwriting" in the above [Data flow](#data-flow).
+To archive those legacy job traces, please follow the instruction below.
+
+1. Execute the following command
+
+ ```bash
+ gitlab-rake gitlab:traces:archive
+ ```
+
+ After you executed this task, GitLab instance queues up Sidekiq jobs (asynchronous processes)
+ for migrating job trace files from local storage to object storage.
+ It could take time to complete the all migration jobs. You can check the progress by the following command
+
+ ```bash
+ sudo gitlab-rails console
+ ```
+
+ ```bash
+ [1] pry(main)> Sidekiq::Stats.new.queues['pipeline_background:archive_trace']
+ => 100
+ ```
+
+ If the count becomes zero, the archiving processes are done
+
+## How to migrate archived job traces to object storage
+
+If job traces have already been archived into local storage, and you want to migrate those traces to object storage, please follow the instruction below.
+
+1. Ensure [Object storage integration for Job Artifacts](job_artifacts.md#object-storage-settings) is enabled
+1. Execute the following command
+
+ ```bash
+ gitlab-rake gitlab:traces:migrate
+ ```
+
+## How to remove job traces
+
+There isn't a way to automatically expire old job logs, but it's safe to remove
+them if they're taking up too much space. If you remove the logs manually, the
+job output in the UI will be empty.
## New live trace architecture
diff --git a/doc/administration/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 88221db78f1..bd758c49eba 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -42,7 +42,7 @@ Registry, etc.
## Hashed Storage
> **Warning:** Hashed storage is in **Beta**. For the latest updates, check the
-> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821)
+> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/3542)
> and please report any problems you encounter.
Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead
diff --git a/doc/ci/docker/README.md b/doc/ci/docker/README.md
index b0e01d74f7e..8ae80b2bc02 100644
--- a/doc/ci/docker/README.md
+++ b/doc/ci/docker/README.md
@@ -6,3 +6,4 @@ comments: false
- [Using Docker Images](using_docker_images.md)
- [Using Docker Build](using_docker_build.md)
+- [Using kaniko](using_kaniko.md)
diff --git a/doc/ci/docker/using_kaniko.md b/doc/ci/docker/using_kaniko.md
new file mode 100644
index 00000000000..7d4f28e1f47
--- /dev/null
+++ b/doc/ci/docker/using_kaniko.md
@@ -0,0 +1,60 @@
+# Building images with kaniko and GitLab CI/CD
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45512) in GitLab 11.2.
+Requires GitLab Runner 11.2 and above.
+
+[kaniko](https://github.com/GoogleContainerTools/kaniko) is a tool to build
+container images from a Dockerfile, inside a container or Kubernetes cluster.
+
+kaniko solves two problems with using the
+[docker-in-docker build](using_docker_build.md#use-docker-in-docker-executor) method:
+
+1. Docker-in-docker requires [privileged mode](https://docs.docker.com/engine/reference/run/#runtime-privilege-and-linux-capabilities)
+ in order to function, which is a significant security concern.
+1. Docker-in-docker generally incurs a performance penalty and can be quite slow.
+
+## Requirements
+
+In order to utilize kaniko with GitLab, a [GitLab Runner](https://docs.gitlab.com/runner/)
+using either the [Kubernetes](https://docs.gitlab.com/runner/executors/kubernetes.html),
+[Docker](https://docs.gitlab.com/runner/executors/docker.html), or
+[Docker Machine](https://docs.gitlab.com/runner/executors/docker_machine.html)
+executors is required.
+
+## Building a Docker image with kaniko
+
+When building an image with kaniko and GitLab CI/CD, you should be aware of a
+few important details:
+
+- The kaniko debug image is recommended (`gcr.io/kaniko-project/executor:debug`)
+ because it has a shell, and a shell is required for an image to be used with
+ GitLab CI/CD.
+- The entrypoint will need to be [overridden](using_docker_images.md#overriding-the-entrypoint-of-an-image),
+ otherwise the build script will not run.
+- A Docker `config.json` file needs to be created with the authentication
+ information for the desired container registry.
+
+---
+
+In the following example, kaniko is used to build a Docker image and then push
+it to [GitLab Container Registry](../../user/project/container_registry.md).
+The job will run only when a tag is pushed. A `config.json` file is created under
+`/root/.docker` with the needed GitLab Container Registry credentials taken from the
+[environment variables](../variables/README.md#predefined-variables-environment-variables)
+GitLab CI/CD provides. In the last step, kaniko uses the `Dockerfile` under the
+root directory of the project, builds the Docker image and pushes it to the
+project's Container Registry while tagging it with the Git tag:
+
+```yaml
+build:
+ stage: build
+ image:
+ name: gcr.io/kaniko-project/executor:debug
+ entrypoint: [""]
+ script:
+ - mkdir -p /root/.docker
+ - echo "{\"auths\":{\"$CI_REGISTRY\":{\"username\":\"$CI_REGISTRY_USER\",\"password\":\"$CI_REGISTRY_PASSWORD\"}}}" > /root/.docker/config.json
+ - /kaniko/executor --context $CI_PROJECT_DIR --dockerfile $CI_PROJECT_DIR/Dockerfile --destination $CI_REGISTRY_IMAGE:$CI_COMMIT_TAG
+ only:
+ - tags
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ef740ab1c5e..abba748db8b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1075,8 +1075,10 @@ keep artifacts forever.
After their expiry, artifacts are deleted hourly by default (via a cron job),
and are not accessible anymore.
-The value of `expire_in` is an elapsed time. Examples of parsable values:
+The value of `expire_in` is an elapsed time in seconds, unless a unit is
+provided. Examples of parsable values:
+- '42'
- '3 mins 4 sec'
- '2 hrs 20 min'
- '2h20min'
diff --git a/doc/development/README.md b/doc/development/README.md
index fed3903c771..ee9a9852205 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -55,7 +55,13 @@ description: 'Learn how to contribute to GitLab.'
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
-## Databases guides
+## Database guides
+
+### Tooling
+
+- [Understanding EXPLAIN plans](understanding_explain_plans.md)
+- [explain.depesz.com](https://explain.depesz.com/) for visualising the output
+ of `EXPLAIN`
### Migrations
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 32de741c9fe..1cd873b6fe3 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -258,6 +258,31 @@ end
[`extend ::Gitlab::Utils::Override`]: utilities.md#override
+##### Overriding CE class methods
+
+The same applies to class methods, except we want to use
+`ActiveSupport::Concern` and put `extend ::Gitlab::Utils::Override`
+within the block of `class_methods`. Here's an example:
+
+```ruby
+module EE
+ module Groups
+ module GroupMembersController
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend ::Gitlab::Utils::Override
+
+ override :admin_not_required_endpoints
+ def admin_not_required_endpoints
+ super.concat(%i[update override])
+ end
+ end
+ end
+ end
+end
+```
+
#### Use self-descriptive wrapper methods
When it's not possible/logical to modify the implementation of a
@@ -665,6 +690,9 @@ module EE
extend ActiveSupport::Concern
class_methods do
+ extend ::Gitlab::Utils::Override
+
+ override :update_params_at_least_one_of
def update_params_at_least_one_of
super.push(*%i[
squash
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
new file mode 100644
index 00000000000..adf8795a5e3
--- /dev/null
+++ b/doc/development/understanding_explain_plans.md
@@ -0,0 +1,676 @@
+# Understanding EXPLAIN plans
+
+PostgreSQL allows you to obtain query plans using the `EXPLAIN` command. This
+command can be invaluable when trying to determine how a query will perform.
+You can use this command directly in your SQL query, as long as the query starts
+with it:
+
+```sql
+EXPLAIN
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+When running this on GitLab.com, we are presented with the following output:
+
+```
+Aggregate (cost=922411.76..922411.77 rows=1 width=8)
+ -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+```
+
+When using _just_ `EXPLAIN`, PostgreSQL won't actually execute our query,
+instead it produces an _estimated_ execution plan based on the available
+statistics. This means the actual plan can differ quite a bit. Fortunately,
+PostgreSQL provides us with the option to execute the query as well. To do so,
+we need to use `EXPLAIN ANALYZE` instead of just `EXPLAIN`:
+
+```sql
+EXPLAIN ANALYZE
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+This will produce:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+As we can see this plan is quite different, and includes a lot more data. Let's
+discuss this step by step.
+
+Because `EXPLAIN ANALYZE` executes the query, care should be taken when using a
+query that will write data or might time out. If the query modifies data,
+consider wrapping it in a transaction that rolls back automatically like so:
+
+```sql
+BEGIN;
+EXPLAIN ANALYZE
+DELETE FROM users WHERE id = 1;
+ROLLBACK;
+```
+
+The `EXPLAIN` command also takes additional options, such as `BUFFERS`:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+This will then produce:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ Buffers: shared hit=208846
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+For more information, refer to the official [EXPLAIN
+documentation](https://www.postgresql.org/docs/current/static/sql-explain.html).
+
+## Nodes
+
+Every query plan consists of nodes. Nodes can be nested, and are executed from
+the inside out. This means that the innermost node is executed before an outer
+node. This can be best thought of as nested function calls, returning their
+results as they unwind. For example, a plan starting with an `Aggregate`
+followed by a `Nested Loop`, followed by an `Index Only scan` can be thought of
+as the following Ruby code:
+
+```ruby
+aggregate(
+ nested_loop(
+ index_only_scan()
+ index_only_scan()
+ )
+)
+```
+
+Nodes are indicated using a `->` followed by the type of node taken. For
+example:
+
+```
+Aggregate (cost=922411.76..922411.77 rows=1 width=8)
+ -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+```
+
+Here the first node executed is `Seq scan on projects`. The `Filter:` is an
+additional filter applied to the results of the node. A filter is very similar
+to Ruby's `Array#select`: it takes the input rows, applies the filter, and
+produces a new list of rows. Once the node is done, we perform the `Aggregate`
+above it.
+
+Nested nodes will look like this:
+
+```
+Aggregate (cost=176.97..176.98 rows=1 width=8) (actual time=0.252..0.252 rows=1 loops=1)
+ Buffers: shared hit=155
+ -> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1)
+ Buffers: shared hit=155
+ -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1)
+ Index Cond: (id < 100)
+ Heap Fetches: 0
+ -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36)
+ Index Cond: (id = users_1.id)
+ Heap Fetches: 0
+Planning time: 2.585 ms
+Execution time: 0.310 ms
+```
+
+Here we first perform two separate "Index Only" scans, followed by performing a
+"Nested Loop" on the result of these two scans.
+
+## Node statistics
+
+Each node in a plan has a set of associated statistics, such as the cost, the
+number of rows produced, the number of loops performed, and more. For example:
+
+```
+Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+```
+
+Here we can see that our cost ranges from `0.00..908044.47` (we'll cover this in
+a moment), and we estimate (since we're using `EXPLAIN` and not `EXPLAIN
+ANALYZE`) a total of 5,746,914 rows to be produced by this node. The `width`
+statistics describes the estimated width of each row, in bytes.
+
+The `costs` field specifies how expensive a node was. The cost is measured in
+arbitrary units determined by the query planner's cost parameters. What
+influences the costs depends on a variety of settings, such as `seq_page_cost`,
+`cpu_tuple_cost`, and various others.
+The format of the costs field is as follows:
+
+```
+STARTUP COST..TOTAL COST
+```
+
+The startup cost states how expensive it was to start the node, with the total
+cost describing how expensive the entire node was. In general: the greater the
+values, the more expensive the node.
+
+When using `EXPLAIN ANALYZE`, these statistics will also include the actual time
+(in milliseconds) spent, and other runtime statistics (e.g. the actual number of
+produced rows):
+
+```
+Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+```
+
+Here we can see we estimated 5,746,969 rows to be returned, but in reality we
+returned 5,746,940 rows. We can also see that _just_ this sequential scan took
+2.98 seconds to run.
+
+Using `EXPLAIN (ANALYZE, BUFFERS)` will also give us information about the
+number of rows removed by a filter, the number of buffers used, and more. For
+example:
+
+```
+Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+```
+
+Here we can see that our filter has to remove 65,677 rows, and that we use
+208,846 buffers. Each buffer in PostgreSQL is 8 KB (8192 bytes), meaning our
+above node uses *1.6 GB of buffers*. That's a lot!
+
+## Node types
+
+There are quite a few different types of nodes, so we only cover some of the
+more common ones here.
+
+A full list of all the available nodes and their descriptions can be found in
+the [PostgreSQL source file
+"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h)
+
+### Seq Scan
+
+A sequential scan over (a chunk of) a database table. This is like using
+`Array#each`, but on a database table. Sequential scans can be quite slow when
+retrieving lots of rows, so it's best to avoid these for large tables.
+
+### Index Only Scan
+
+A scan on an index that did not require fetching anything from the table. In
+certain cases an index only scan may still fetch data from the table, in this
+case the node will include a `Heap Fetches:` statistic.
+
+### Index Scan
+
+A scan on an index that required retrieving some data from the table.
+
+### Bitmap Index Scan and Bitmap Heap scan
+
+Bitmap scans fall between sequential scans and index scans. These are typically
+used when we would read too much data from an index scan, but too little to
+perform a sequential scan. A bitmap scan uses what is known as a [bitmap
+index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work.
+
+The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457)
+states the following on bitmap scans:
+
+> Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not
+> access the heap itself. The bitmap is used by an ancestor Bitmap Heap Scan
+> node, possibly after passing through intermediate Bitmap And and/or Bitmap Or
+> nodes to combine it with the results of other Bitmap Index Scans.
+
+### Limit
+
+Applies a `LIMIT` on the input rows.
+
+### Sort
+
+Sorts the input rows as specified using an `ORDER BY` statement.
+
+### Nested Loop
+
+A nested loop will execute its child nodes for every row produced by a node that
+precedes it. For example:
+
+```
+-> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1)
+ Buffers: shared hit=155
+ -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1)
+ Index Cond: (id < 100)
+ Heap Fetches: 0
+ -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36)
+ Index Cond: (id = users_1.id)
+ Heap Fetches: 0
+```
+
+Here the first child node (`Index Only Scan using users_pkey on users users_1`)
+produces 36 rows, and is executed once (`rows=36 loops=1`). The next node
+produces 1 row (`rows=1`), but is repeated 36 times (`loops=36`). This is
+because the previous node produced 36 rows.
+
+This means that nested loops can quickly slow the query down if the various
+child nodes keep producing many rows.
+
+## Optimising queries
+
+With that out of the way, let's see how we can optimise a query. Let's use the
+following query as an example:
+
+```sql
+SELECT COUNT(*)
+FROM users
+WHERE twitter != '';
+```
+
+This query simply counts the number of users that have a Twitter profile set.
+Let's run this using `EXPLAIN (ANALYZE, BUFFERS)`:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM users
+WHERE twitter != '';
+```
+
+This will produce the following plan:
+
+```
+Aggregate (cost=845110.21..845110.22 rows=1 width=8) (actual time=1271.157..1271.158 rows=1 loops=1)
+ Buffers: shared hit=202662
+ -> Seq Scan on users (cost=0.00..844969.99 rows=56087 width=0) (actual time=0.019..1265.883 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487813
+ Buffers: shared hit=202662
+Planning time: 0.390 ms
+Execution time: 1271.180 ms
+```
+
+From this query plan we can see the following:
+
+1. We need to perform a sequential scan on the `users` table.
+1. This sequential scan filters out 2,487,813 rows using a `Filter`.
+1. We use 202,622 buffers, which equals 1.58 GB of memory.
+1. It takes us 1.2 seconds to do all of this.
+
+Considering we are just counting users, that's quite expensive!
+
+Before we start making any changes, let's see if there are any existing indexes
+on the `users` table that we might be able to use. We can obtain this
+information by running `\d users` in a `psql` console, then scrolling down to
+the `Indexes:` section:
+
+```
+Indexes:
+ "users_pkey" PRIMARY KEY, btree (id)
+ "users_confirmation_token_key" UNIQUE CONSTRAINT, btree (confirmation_token)
+ "users_email_key" UNIQUE CONSTRAINT, btree (email)
+ "users_reset_password_token_key" UNIQUE CONSTRAINT, btree (reset_password_token)
+ "index_on_users_lower_email" btree (lower(email::text))
+ "index_on_users_lower_username" btree (lower(username::text))
+ "index_on_users_name_lower" btree (lower(name::text))
+ "index_users_on_admin" btree (admin)
+ "index_users_on_created_at" btree (created_at)
+ "index_users_on_email_trigram" gin (email gin_trgm_ops)
+ "index_users_on_feed_token" btree (feed_token)
+ "index_users_on_ghost" btree (ghost)
+ "index_users_on_incoming_email_token" btree (incoming_email_token)
+ "index_users_on_name" btree (name)
+ "index_users_on_name_trigram" gin (name gin_trgm_ops)
+ "index_users_on_state" btree (state)
+ "index_users_on_state_and_internal_attrs" btree (state) WHERE ghost <> true AND support_bot <> true
+ "index_users_on_support_bot" btree (support_bot)
+ "index_users_on_username" btree (username)
+ "index_users_on_username_trigram" gin (username gin_trgm_ops)
+```
+
+Here we can see there is no index on the `twitter` column, which means
+PostgreSQL has to perform a sequential scan in this case. Let's try to fix this
+by adding the following index:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter);
+```
+
+If we now re-run our query using `EXPLAIN (ANALYZE, BUFFERS)` we get the
+following plan:
+
+```
+Aggregate (cost=61002.82..61002.83 rows=1 width=8) (actual time=297.311..297.312 rows=1 loops=1)
+ Buffers: shared hit=51854 dirtied=19
+ -> Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487830
+ Heap Fetches: 26037
+ Buffers: shared hit=51854 dirtied=19
+Planning time: 0.191 ms
+Execution time: 297.334 ms
+```
+
+Now it takes just under 300 milliseconds to get our data, instead of 1.2
+seconds. However, we still use 51,854 buffers, which is about 400 MB of memory.
+300 milliseconds is also quite slow for such a simple query. To understand why
+this query is still expensive, let's take a look at the following:
+
+```
+Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487830
+```
+
+We start with an index only scan on our index, but we somehow still apply a
+`Filter` that filters out 2,487,830 rows. Why is that? Well, let's look at how
+we created the index:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter);
+```
+
+We simply told PostgreSQL to index all possible values of the `twitter` column,
+even empty strings. Our query in turn uses `WHERE twitter != ''`. This means
+that the index does improve things, as we don't need to do a sequential scan,
+but we may still encounter empty strings. This means PostgreSQL _has_ to apply a
+Filter on the index results to get rid of those values.
+
+Fortunately, we can improve this even further using "partial indexes". Partial
+indexes are indexes with a `WHERE` condition that is applied when indexing data.
+For example:
+
+```sql
+CREATE INDEX CONCURRENTLY some_index ON users (email) WHERE id < 100
+```
+
+This index would only index the `email` value of rows that match `WHERE id <
+100`. We can use partial indexes to change our Twitter index to the following:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter) WHERE twitter != '';
+```
+
+Once created, if we run our query again we will be given the following plan:
+
+```
+Aggregate (cost=1608.26..1608.27 rows=1 width=8) (actual time=19.821..19.821 rows=1 loops=1)
+ Buffers: shared hit=44036
+ -> Index Only Scan using twitter_test on users (cost=0.41..1479.71 rows=51420 width=0) (actual time=0.023..15.514 rows=51833 loops=1)
+ Heap Fetches: 1208
+ Buffers: shared hit=44036
+Planning time: 0.123 ms
+Execution time: 19.848 ms
+```
+
+That's _a lot_ better! Now it only takes 20 milliseconds to get the data, and we
+only use about 344 MB of buffers (instead of the original 1.58 GB). The reason
+this works is that now PostgreSQL no longer needs to apply a `Filter`, as the
+index only contains `twitter` values that are not empty.
+
+Keep in mind that you shouldn't just add partial indexes every time you want to
+optimise a query. Every index has to be updated for every write, and they may
+require quite a bit of space, depending on the amount of indexed data. As a
+result, first check if there are any existing indexes you may be able to reuse.
+If there aren't any, check if you can perhaps slightly change an existing one to
+fit both the existing and new queries. Only add a new index if none of the
+existing indexes can be used in any way.
+
+## Queries that can't be optimised
+
+Now that we have seen how to optimise a query, let's look at another query that
+we might not be able to optimise:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+The output of `EXPLAIN (ANALYZE, BUFFERS)` is as follows:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ Buffers: shared hit=208846
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+Looking at the output we see the following Filter:
+
+```
+Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+Rows Removed by Filter: 65677
+```
+
+Looking at the number of rows removed by the filter, we may be tempted to add an
+index on `projects.visibility_level` to somehow turn this Sequential scan +
+filter into an index-only scan.
+
+Unfortunately, doing so is unlikely to improve anything. Contrary to what some
+might believe, an index being present _does not guarantee_ that PostgreSQL will
+actually use it. For example, when doing a `SELECT * FROM projects` it is much
+cheaper to just scan the entire table, instead of using an index and then
+fetching data from the table. In such cases PostgreSQL may decide to not use an
+index.
+
+Second, let's think for a moment what our query does: it gets all projects with
+visibility level 0 or 20. In the above plan we can see this produces quite a lot
+of rows (5,745,940), but how much is that relative to the total? Let's find out
+by running the following query:
+
+```sql
+SELECT visibility_level, count(*) AS amount
+FROM projects
+GROUP BY visibility_level
+ORDER BY visibility_level ASC;
+```
+
+For GitLab.com this produces:
+
+```
+ visibility_level | amount
+------------------+---------
+ 0 | 5071325
+ 10 | 65678
+ 20 | 674801
+```
+
+Here the total number of projects is 5,811,804, and 5,746,126 of those are of
+level 0 or 20. That's 98% of the entire table!
+
+So no matter what we do, this query will retrieve 98% of the entire table. Since
+most time is spent doing exactly that, there isn't really much we can do to
+improve this query, other than _not_ running it at all.
+
+What is important here is that while some may recommend to straight up add an
+index the moment you see a sequential scan, it is _much more important_ to first
+understand what your query does, how much data it retrieves, and so on. After
+all, you can not optimise something you do not understand.
+
+### Cardinality and selectivity
+
+Earlier we saw that our query had to retrieve 98% of the rows in the table.
+There are two terms commonly used for databases: cardinality, and selectivity.
+Cardinality refers to the number of unique values in a particular column in a
+table.
+
+Selectivity is the number of unique values produced by an operation (e.g. an
+index scan or filter), relative to the total number of rows. The higher the
+selectivity, the more likely PostgreSQL is able to use an index.
+
+In the above example, there are only 3 unique values: 0, 10, and 20. This means
+the cardinality is 3. The selectivity in turn is also very low: 0.0000003% (2 /
+5,811,804), because our `Filter` only filters using two values (`0` and `20`).
+With such a low selectivity value it's not surprising that PostgreSQL decides
+using an index is not worth it, because it would produce almost no unique rows.
+
+## Rewriting queries
+
+So the above query can't really be optimised as-is, or at least not much. But
+what if we slightly change the purpose of it? What if instead of retrieving all
+projects with `visibility_level` 0 or 20, we retrieve those that a user
+interacted with somehow?
+
+Fortunately, GitLab has an answer for this, and it's a table called
+`user_interacted_projects`. This table has the following schema:
+
+```
+Table "public.user_interacted_projects"
+ Column | Type | Modifiers
+------------+---------+-----------
+ user_id | integer | not null
+ project_id | integer | not null
+Indexes:
+ "index_user_interacted_projects_on_project_id_and_user_id" UNIQUE, btree (project_id, user_id)
+ "index_user_interacted_projects_on_user_id" btree (user_id)
+Foreign-key constraints:
+ "fk_rails_0894651f08" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ "fk_rails_722ceba4f7" FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
+```
+
+Let's rewrite our query to JOIN this table onto our projects, and get the
+projects for a specific user:
+
+```sql
+EXPLAIN ANALYZE
+SELECT COUNT(*)
+FROM projects
+INNER JOIN user_interacted_projects ON user_interacted_projects.project_id = projects.id
+WHERE projects.visibility_level IN (0, 20)
+AND user_interacted_projects.user_id = 1;
+```
+
+What we do here is the following:
+
+1. Get our projects.
+1. INNER JOIN `user_interacted_projects`, meaning we're only left with rows in
+ `projects` that have a corresponding row in `user_interacted_projects`.
+1. Limit this to the projects with `visibility_level` of 0 or 20, and to
+ projects that the user with ID 1 interacted with.
+
+If we run this query we get the following plan:
+
+```
+ Aggregate (cost=871.03..871.04 rows=1 width=8) (actual time=9.763..9.763 rows=1 loops=1)
+ -> Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1)
+ -> Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1)
+ Index Cond: (user_id = 1)
+ -> Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+ Index Cond: (id = user_interacted_projects.project_id)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 0
+ Planning time: 2.614 ms
+ Execution time: 9.809 ms
+```
+
+Here it only took us just under 10 milliseconds to get the data. We can also see
+we're retrieving far fewer projects:
+
+```
+Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+ Index Cond: (id = user_interacted_projects.project_id)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 0
+```
+
+Here we see we perform 145 loops (`loops=145`), with every loop producing 1 row
+(`rows=1`). This is much less than before, and our query performs much better!
+
+If we look at the plan we also see our costs are very low:
+
+```
+Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+```
+
+Here our cost is only 3.45, and it only takes us 0.050 milliseconds to do so.
+The next index scan is a bit more expensive:
+
+```
+Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1)
+```
+
+Here the cost is 160.71 (`cost=0.43..160.71`), taking about 2.5 milliseconds
+(based on the output of `actual time=....`).
+
+The most expensive part here is the "Nested Loop" that acts upon the result of
+these two index scans:
+
+```
+Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1)
+```
+
+Here we had to perform 870.52 disk page fetches for 203 rows, 9.748
+milliseconds, producing 143 rows in a single loop.
+
+The key takeaway here is that sometimes you have to rewrite (parts of) a query
+to make it better. Sometimes that means having to slightly change your feature
+to accommodate for better performance.
+
+## What makes a bad plan
+
+This is a bit of a difficult question to answer, because the definition of "bad"
+is relative to the problem you are trying to solve. However, some patterns are
+best avoided in most cases, such as:
+
+* Sequential scans on large tables
+* Filters that remove a lot of rows
+* Performing a certain step (e.g. an index scan) that requires _a lot_ of
+ buffers (e.g. more than 512 MB for GitLab.com).
+
+As a general guideline, aim for a query that:
+
+1. Takes no more than 10 milliseconds. Our target time spent in SQL per request
+ is around 100 milliseconds, so every query should be as fast as possible.
+1. Does not use an excessive number of buffers, relative to the workload. For
+ example, retrieving ten rows shouldn't require 1 GB of buffers.
+1. Does not spend a long amount of time performing disk IO operations. The
+ setting `track_io_timing` must be enabled for this data to be included in the
+ output of `EXPLAIN ANALYZE`.
+1. Applies a `LIMIT` when retrieving rows without aggregating them, such as
+ `SELECT * FROM users`.
+1. Doesn't use a `Filter` to filter out too many rows, especially if the query
+ does not use a `LIMIT` to limit the number of returned rows. Filters can
+ usually be removed by adding a (partial) index.
+
+These are _guidelines_ and not hard requirements, as different needs may require
+different queries. The only _rule_ is that you _must always measure_ your query
+(preferably using a production-like database) using `EXPLAIN (ANALYZE, BUFFERS)`
+and related tools such as:
+
+* <https://explain.depesz.com/>
+* <http://tatiyants.com/postgres-query-plan-visualization/>
+
+GitLab employees can also use our chatops solution, available in Slack using the
+`/chatops` slash command. You can use chatops to get a query plan by running the
+following:
+
+```
+/chatops run explain SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20)
+```
+
+Visualising the plan using <https://explain.depesz.com/> is also supported:
+
+```
+/chatops run explain --visual SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20)
+```
+
+Quoting the query is not necessary.
+
+For more information about the available options, run:
+
+```
+/chatops run explain --help
+```
diff --git a/doc/topics/autodevops/index.md b/doc/topics/autodevops/index.md
index f5574506595..0474182e324 100644
--- a/doc/topics/autodevops/index.md
+++ b/doc/topics/autodevops/index.md
@@ -233,6 +233,11 @@ in **Admin Area > Settings > Continuous Integration and Deployment**. Doing that
all the projects that haven't explicitly set an option will have Auto DevOps
enabled by default.
+NOTE: **Note:**
+There is also a feature flag to enable Auto DevOps to a percentage of projects
+which can be enabled from the console with
+`Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(10)`.
+
### Deployment strategy
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/38542) in GitLab 11.0.
diff --git a/doc/user/admin_area/img/cohorts.png b/doc/user/admin_area/img/cohorts.png
deleted file mode 100644
index 8bae7faff07..00000000000
--- a/doc/user/admin_area/img/cohorts.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/monitoring/convdev.md b/doc/user/admin_area/monitoring/convdev.md
index a98602c4d70..6ad8a5a7ff0 100644
--- a/doc/user/admin_area/monitoring/convdev.md
+++ b/doc/user/admin_area/monitoring/convdev.md
@@ -1,29 +1,5 @@
-# Conversational Development Index
+---
+redirect_to: '../../instance_statistics/convdev.md'
+---
-> [Introduced][ce-30469] in GitLab 9.3.
-
-Conversational Development Index (ConvDev) gives you an overview of your entire
-instance's feature usage, from idea to production. It looks at your usage in the
-past 30 days, averaged over the number of active users in that time period. It also
-provides a lead score per feature, which is calculated based on GitLab's analysis
-of top performing instances, based on [usage ping data][ping] that GitLab has
-collected. Your score is compared to the lead score, expressed as a percentage.
-The overall index score is an average over all your feature scores.
-
-![ConvDev index](img/convdev_index.png)
-
-The page also provides helpful links to articles and GitLab docs, to help you
-improve your scores.
-
-Your GitLab instance's usage ping must be activated in order to use this feature.
-Usage ping data is aggregated on GitLab's servers for analysis. Your usage
-information is **not sent** to any other GitLab instances.
-
-If you have just started using GitLab, it may take a few weeks for data to be
-collected before this feature is available.
-
-This feature is accessible only to a system admin, at
-**Admin area > Overview > ConvDev Index**.
-
-[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469
-[ping]: ../settings/usage_statistics.md#usage-ping
+This document was moved to [another location](../../instance_statistics/convdev.md).
diff --git a/doc/user/admin_area/monitoring/img/convdev_index.png b/doc/user/admin_area/monitoring/img/convdev_index.png
deleted file mode 100644
index 1bf1d6a83c9..00000000000
--- a/doc/user/admin_area/monitoring/img/convdev_index.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/admin_area/settings/continuous_integration.md b/doc/user/admin_area/settings/continuous_integration.md
index eb6f915f3f4..76d9a4ceb03 100644
--- a/doc/user/admin_area/settings/continuous_integration.md
+++ b/doc/user/admin_area/settings/continuous_integration.md
@@ -21,8 +21,9 @@ that this setting is set for each job.
The default expiration time of the [job artifacts][art-yml] can be set in
the Admin area of your GitLab instance. The syntax of duration is described
in [artifacts:expire_in][duration-syntax]. The default is `30 days`. Note that
-this setting is set for each job. Set it to 0 if you don't want default
-expiration.
+this setting is set for each job. Set it to `0` if you don't want default
+expiration. The default unit is in seconds.
+
1. Go to **Admin area > Settings** (`/admin/application_settings`).
diff --git a/doc/user/admin_area/settings/usage_statistics.md b/doc/user/admin_area/settings/usage_statistics.md
index 381efdf5d67..b7427592e10 100644
--- a/doc/user/admin_area/settings/usage_statistics.md
+++ b/doc/user/admin_area/settings/usage_statistics.md
@@ -6,7 +6,7 @@ to perform various actions.
All statistics are opt-out, you can enable/disable them from the admin panel
under **Admin area > Settings > Usage statistics**.
-## Version check
+## Version check **[CORE ONLY]**
If enabled, version check will inform you if a new version is available and the
importance of it through a status. This is shown on the help page (i.e. `/help`)
@@ -29,7 +29,7 @@ secure.
If you disable version check, this information will not be collected. Enable or
disable the version check at **Admin area > Settings > Usage statistics**.
-## Usage ping
+## Usage ping **[CORE ONLY]**
> [Introduced][ee-557] in GitLab Enterprise Edition 8.10. More statistics
[were added][ee-735] in GitLab Enterprise Edition
@@ -67,6 +67,15 @@ production: &base
usage_ping_enabled: false
```
+## Instance statistics visibility **[CORE ONLY]**
+
+Once usage ping is enabled, GitLab will gather data from other instances and
+will be able to show [usage statistics](../../instance_statistics/index.md)
+of your instance to your users.
+
+This can be restricted to admins by selecting "Only admins" in the Instance
+Statistics visibility section under **Admin area > Settings > Usage statistics**.
+
[ee-557]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/557
[ee-735]: https://gitlab.com/gitlab-org/gitlab-ee/merge_requests/735
[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
diff --git a/doc/user/admin_area/user_cohorts.md b/doc/user/admin_area/user_cohorts.md
index e25e7a8bbc3..21e61e2ec44 100644
--- a/doc/user/admin_area/user_cohorts.md
+++ b/doc/user/admin_area/user_cohorts.md
@@ -1,37 +1,5 @@
-# Cohorts
+---
+redirect_to: '../instance_statistics/user_cohorts.md'
+---
-> **Notes:**
-> [Introduced][ce-23361] in GitLab 9.1.
-
-As a benefit of having the [usage ping active](settings/usage_statistics.md),
-GitLab lets you analyze the users' activities of your GitLab installation.
-Under `/admin/cohorts`, when the usage ping is active, GitLab will show the
-monthly cohorts of new users and their activities over time.
-
-## Overview
-
-How do we read the user cohorts table? Let's take an example with the following
-user cohorts.
-
-![User cohort example](img/cohorts.png)
-
-For the cohort of June 2016, 163 users have been added on this server and have
-been active since this month. One month later, in July 2016, out of
-these 163 users, 155 users (or 95% of the June cohort) are still active. Two
-months later, 139 users (or 85%) are still active. 9 months later, we can see
-that only 6% of this cohort are still active.
-
-The Inactive users column shows the number of users who have been added during
-the month, but who have never actually had any activity in the instance.
-
-How do we measure the activity of users? GitLab considers a user active if:
-
-* the user signs in
-* the user has Git activity (whether push or pull).
-
-## Setup
-
-1. [Activate the usage ping](settings/usage_statistics.md)
-2. Go to `/admin/cohorts` to see the user cohorts of the server
-
-[ce-23361]: https://gitlab.com/gitlab-org/gitlab-ce/issues/23361
+This document was moved to [another location](../instance_statistics/user_cohorts.md).
diff --git a/doc/user/index.md b/doc/user/index.md
index 90f0e2285c3..649c0b664a5 100644
--- a/doc/user/index.md
+++ b/doc/user/index.md
@@ -172,3 +172,7 @@ Automate GitLab via [API](../api/README.md).
## Git and GitLab
Learn what is [Git](../topics/git/index.md) and its best practices.
+
+## Instance statistics
+
+See [various statistics](instance_statistics/index.md) of your GitLab instance.
diff --git a/doc/user/instance_statistics/convdev.md b/doc/user/instance_statistics/convdev.md
new file mode 100644
index 00000000000..d2795e092fc
--- /dev/null
+++ b/doc/user/instance_statistics/convdev.md
@@ -0,0 +1,26 @@
+# Conversational Development Index
+
+> [Introduced][ce-30469] in GitLab 9.3.
+
+Conversational Development Index (ConvDev) gives you an overview of your entire
+instance's feature usage, from idea to production. It looks at your usage in the
+past 30 days, averaged over the number of active users in that time period. It also
+provides a lead score per feature, which is calculated based on GitLab's analysis
+of top performing instances, based on [usage ping data][ping] that GitLab has
+collected. Your score is compared to the lead score, expressed as a percentage.
+The overall index score is an average over all your feature scores.
+
+![ConvDev index](img/convdev_index.png)
+
+The page also provides helpful links to articles and GitLab docs, to help you
+improve your scores.
+
+Your GitLab instance's usage ping must be activated in order to use this feature.
+Usage ping data is aggregated on GitLab's servers for analysis. Your usage
+information is **not sent** to any other GitLab instances.
+
+If you have just started using GitLab, it may take a few weeks for data to be
+collected before this feature is available.
+
+[ce-30469]: https://gitlab.com/gitlab-org/gitlab-ce/issues/30469
+[ping]: ../admin_area/settings/usage_statistics.md#usage-ping
diff --git a/doc/user/instance_statistics/img/cohorts.png b/doc/user/instance_statistics/img/cohorts.png
new file mode 100644
index 00000000000..12e839e7cd2
--- /dev/null
+++ b/doc/user/instance_statistics/img/cohorts.png
Binary files differ
diff --git a/doc/user/instance_statistics/img/convdev_index.png b/doc/user/instance_statistics/img/convdev_index.png
new file mode 100644
index 00000000000..191295c918b
--- /dev/null
+++ b/doc/user/instance_statistics/img/convdev_index.png
Binary files differ
diff --git a/doc/user/instance_statistics/img/instance_statistics_button.png b/doc/user/instance_statistics/img/instance_statistics_button.png
new file mode 100644
index 00000000000..6104321b1a6
--- /dev/null
+++ b/doc/user/instance_statistics/img/instance_statistics_button.png
Binary files differ
diff --git a/doc/user/instance_statistics/index.md b/doc/user/instance_statistics/index.md
new file mode 100644
index 00000000000..a4eca89b7fe
--- /dev/null
+++ b/doc/user/instance_statistics/index.md
@@ -0,0 +1,19 @@
+# Instance statistics
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/41416)
+in GitLab 11.2.
+
+Instance statistics gives users or admins access to instance-wide analytics.
+They are accessible to all users by default (GitLab admins can restrict its
+visibility in the [admin area](../admin_area/settings/usage_statistics.md)),
+and can be accessed via the top bar.
+
+![Instance Statistics button](img/instance_statistics_button.png)
+
+For the statistics to show up, [usage ping must be enabled](../admin_area/settings/usage_statistics.md#usage-ping)
+by an admin in the admin settings area.
+
+There are two kinds of statistics:
+
+- [Conversational Development (ConvDev) Index](convdev.md): Provides an overview of your entire instance's feature usage.
+- [User Cohorts](user_cohorts.md): Display the monthly cohorts of new users and their activities over time.
diff --git a/doc/user/instance_statistics/user_cohorts.md b/doc/user/instance_statistics/user_cohorts.md
new file mode 100644
index 00000000000..70d5912dc4e
--- /dev/null
+++ b/doc/user/instance_statistics/user_cohorts.md
@@ -0,0 +1,27 @@
+# Cohorts
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/23361)
+in GitLab 9.1.
+
+As a benefit of having the [usage ping active](../admin_area/settings/usage_statistics.md),
+GitLab lets you analyze the users' activities over time of your GitLab installation.
+
+## Overview
+
+How do we read the user cohorts table? Let's take an example with the following
+user cohorts.
+
+![User cohort example](img/cohorts.png)
+
+For the cohort of Jan 2018, 15 users have been added on this server and have
+been active since this month. One month later, in Feb 2018, all 15 users are
+still active. 6 months later (Month 6, July), we can see 10 users from this cohort
+are active, or 66% of the original cohort of 15 that joined in January.
+
+The Inactive users column shows the number of users who have been added during
+the month, but who have never actually had any activity in the instance.
+
+How do we measure the activity of users? GitLab considers a user active if:
+
+* the user signs in
+* the user has Git activity (whether push or pull).
diff --git a/doc/user/markdown.md b/doc/user/markdown.md
index 6856544ae1b..6203561265b 100644
--- a/doc/user/markdown.md
+++ b/doc/user/markdown.md
@@ -234,7 +234,12 @@ https://gitlab.com/gitlab-org/gitlab-ce/blob/master/doc/user/markdown.md#emoji
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
- Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+ Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
+
+ On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+
+ Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+
Sometimes you want to :monkey: around a bit and add some :star2: to your :speech_balloon:. Well we have a gift for you:
@@ -246,7 +251,13 @@ If you are new to this, don't be :fearful:. You can easily join the emoji :famil
Consult the [Emoji Cheat Sheet](https://www.emojicopy.com) for a list of all supported emoji codes. :thumbsup:
-Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support. On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+Most emoji are natively supported on macOS, Windows, iOS, Android and will fallback to image-based emoji where there is lack of support.
+
+On Linux, you can download [Noto Color Emoji](https://www.google.com/get/noto/help/emoji/) to get full native emoji support.
+
+Ubuntu 18.04 (like many modern Linux distros) has this font installed by default.
+
+
### Special GitLab References
diff --git a/doc/user/project/import/img/manifest_upload.png b/doc/user/project/import/img/manifest_upload.png
deleted file mode 100644
index d6bf4b157dd..00000000000
--- a/doc/user/project/import/img/manifest_upload.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/import/index.md b/doc/user/project/import/index.md
index b55435e5b4f..4ea35a30bbf 100644
--- a/doc/user/project/import/index.md
+++ b/doc/user/project/import/index.md
@@ -11,7 +11,7 @@
1. [From SVN](svn.md)
1. [From TFS](tfs.md)
1. [From repo by URL](repo_by_url.md)
-1. [By uploading a manifest file](manifest.md)
+1. [By uploading a manifest file (AOSP)](manifest.md)
In addition to the specific migration documentation above, you can import any
Git repository via HTTP from the New Project page. Be aware that if the
diff --git a/doc/user/project/import/manifest.md b/doc/user/project/import/manifest.md
index 06171f11e12..24bf6541a9d 100644
--- a/doc/user/project/import/manifest.md
+++ b/doc/user/project/import/manifest.md
@@ -1,36 +1,31 @@
# Import multiple repositories by uploading a manifest file
-GitLab allows you to import all the required git repositories
-based a manifest file like the one used by the [Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml).
-This feature can be very handy when you need to import a project with many repositories like Android Open Source Project (AOSP).
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/28811) in
+GitLab 11.2.
+GitLab allows you to import all the required Git repositories
+based on a manifest file like the one used by the
+[Android repository](https://android.googlesource.com/platform/manifest/+/2d6f081a3b05d8ef7a2b1b52b0d536b2b74feab4/default.xml).
+This feature can be very handy when you need to import a project with many
+repositories like the Android Open Source Project (AOSP).
->**Note:**
-This feature requires [subgroups](../../group/subgroups/index.md) to be supported by your database.
+## Requirements
-You can do it by following next steps:
+GitLab must be using PostgreSQL for its database, since
+[subgroups](../../group/subgroups/index.md) are needed for the manifest import
+to work.
-1. From your GitLab dashboard click **New project**
-1. Switch to the **Import project** tab
-1. Click on the **Manifest file** button
-1. Provide GitLab with a manifest xml file
-1. Select a group you want to import to (you need to create a group first if you don't have one)
-1. Click **List available repositories**
-1. You will be redirected to the import status page with projects list based on manifest file
-1. Check the list and click 'Import all repositories' to start import.
-
-![Manifest upload](img/manifest_upload.png)
+Read more about the [database requirements](../../../install/requirements.md#database).
-![Manifest status](img/manifest_status.png)
+## Manifest format
-### Manifest format
+A manifest must be an XML file. There must be one `remote` tag with a `review`
+attribute that contains a URL to a Git server, and each `project` tag must have
+a `name` and `path` attribute. GitLab will then build the URL to the repository
+by combining the URL from the `remote` tag with a project name.
+A path attribute will be used to represent the project path in GitLab.
-A manifest must be an XML file. There must be one `remote` tag with `review` attribute
-that contains a URL to a git server. Each `project` tag must have `name` and `path` attribute.
-GitLab will build URL to the repository by combining URL from `remote` tag with a project name.
-A path attribute will be used to represent project path in GitLab system.
-
-Below is a valid example of manifest file.
+Below is a valid example of a manifest file:
```xml
<manifest>
@@ -41,9 +36,24 @@ Below is a valid example of manifest file.
</manifest>
```
-As result next projects will be created:
+As a result, the following projects will be created:
| GitLab | Import URL |
|---|---|
-| https://gitlab/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build |
-| https://gitlab/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint |
+| https://gitlab.com/YOUR_GROUP/build/make | https://android-review.googlesource.com/platform/build |
+| https://gitlab.com/YOUR_GROUP/build/blueprint | https://android-review.googlesource.com/platform/build/blueprint |
+
+## Importing the repositories
+
+You can start the import with:
+
+1. From your GitLab dashboard click **New project**
+1. Switch to the **Import project** tab
+1. Click on the **Manifest file** button
+1. Provide GitLab with a manifest xml file
+1. Select a group you want to import to (you need to create a group first if you don't have one)
+1. Click **List available repositories**. At this point, you will be redirected
+ to the import status page with projects list based on the manifest file.
+1. Check the list and click **Import all repositories** to start the import.
+
+ ![Manifest status](img/manifest_status.png)
diff --git a/doc/user/project/integrations/hangouts_chat.md b/doc/user/project/integrations/hangouts_chat.md
index 6ab44420a10..47525617d95 100644
--- a/doc/user/project/integrations/hangouts_chat.md
+++ b/doc/user/project/integrations/hangouts_chat.md
@@ -1,5 +1,7 @@
# Hangouts Chat service
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/43756) in GitLab 11.2.
+
The Hangouts Chat service sends notifications from GitLab to the room for which the webhook was created.
## On Hangouts Chat
diff --git a/doc/user/project/pages/getting_started_part_three.md b/doc/user/project/pages/getting_started_part_three.md
index 61af1d2ab27..e1eede8bbed 100644
--- a/doc/user/project/pages/getting_started_part_three.md
+++ b/doc/user/project/pages/getting_started_part_three.md
@@ -1,5 +1,5 @@
---
-last_updated: 2018-02-16
+last_updated: 2018-08-16
author: Marcia Ramos
author_gitlab: marcia
level: beginner
@@ -28,7 +28,7 @@ Let's start from the beginning with [DNS records](#dns-records).
If you already know how they work and want to skip the introduction to DNS,
you may be interested in skipping it until the [TL;DR](#tl-dr) section below.
-## DNS Records
+### DNS Records
A Domain Name System (DNS) web service routes visitors to websites
by translating domain names (such as `www.example.com`) into the
@@ -64,22 +64,28 @@ for the most popular hosting services:
If your hosting service is not listed above, you can just try to
search the web for `how to add dns record on <my hosting service>`.
-### DNS A record
+#### DNS A record
In case you want to point a root domain (`example.com`) to your
GitLab Pages site, deployed to `namespace.gitlab.io`, you need to
log into your domain's admin control panel and add a DNS `A` record
pointing your domain to Pages' server IP address. For projects on
-GitLab.com, this IP is `52.167.214.135`. For projects living in
+GitLab.com, this IP is `35.185.44.232`. For projects living in
other GitLab instances (CE or EE), please contact your sysadmin
asking for this information (which IP address is Pages server
running on your instance).
**Practical Example:**
-![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated.png)
+![DNS A record pointing to GitLab.com Pages server](img/dns_add_new_a_record_example_updated_2018.png)
-### DNS CNAME record
+NOTE: **Note:**
+Note that if you use your root domain for your GitLab Pages website **only**, and if
+your domain registrar supports this feature, you can add a DNS apex `CNAME`
+record instead of an `A` record. The main advantage of doing so is that when GitLab Pages
+IP on GitLab.com changes for whatever reason, you don't need to update your `A` record.
+
+#### DNS CNAME record
In case you want to point a subdomain (`hello-world.example.com`)
to your GitLab Pages site initially deployed to `namespace.gitlab.io`,
@@ -112,14 +118,14 @@ If the domain has multiple uses (e.g., you host email on it as well):
| From | DNS Record | To |
| ---- | ---------- | -- |
-| domain.com | A | 52.167.214.135 |
+| domain.com | A | 35.185.44.232 |
| domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff |
If the domain is dedicated to GitLab Pages use and no other services run on it:
| From | DNS Record | To |
| ---- | ---------- | -- |
-| subdomain.domain.com | CNAME | gitlab.io |
+| subdomain.domain.com | CNAME | namespace.gitlab.io |
| _gitlab-pages-verification-code.subdomain.domain.com | TXT | gitlab-pages-verification-code=00112233445566778899aabbccddeeff |
> **Notes**:
@@ -129,9 +135,11 @@ If the domain is dedicated to GitLab Pages use and no other services run on it:
> - **Do not** add any special chars after the default Pages
domain. E.g., **do not** point your `subdomain.domain.com` to
`namespace.gitlab.io.` or `namespace.gitlab.io/`.
-> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) from `104.208.235.32` to `52.167.214.135`.
+> - GitLab Pages IP on GitLab.com [was changed](https://about.gitlab.com/2017/03/06/we-are-changing-the-ip-of-gitlab-pages-on-gitlab-com/) in 2017
+> - GitLab Pages IP on GitLab.com [has been changed](https://about.gitlab.com/2018/07/19/gcp-move-update/#gitlab-pages-and-custom-domains)
+from `52.167.214.135` to `35.185.44.232` in 2018
-## Add your custom domain to GitLab Pages settings
+### Add your custom domain to GitLab Pages settings
Once you've set the DNS record, you'll need navigate to your project's
**Setting > Pages** and click **+ New domain** to add your custom domain to
@@ -165,6 +173,18 @@ will fail and attempts to visit your domain will respond with a 404.
Read through the [general documentation on GitLab Pages](introduction.md#add-a-custom-domain-to-your-pages-website) to learn more about adding
custom domains to GitLab Pages sites.
+### Redirecting `www.domain.com` to `domain.com` with Cloudflare
+
+If you use Cloudflare, you can redirect `www` to `domain.com` without the need of adding both
+`www.domain.com` and `domain.com` to GitLab. This happens due to a [Cloudflare feature that creates
+a 301 redirect as a "page rule"](https://gitlab.com/gitlab-org/gitlab-ce/issues/48848#note_87314849) for redirecting `www.domain.com` to `domain.com`. In this case,
+you can use the following setup:
+
+- In Cloudflare, create a DNS `A` record pointing `domain.com` to `35.185.44.232`
+- In GitLab, add the domain to GitLab Pages
+- In Cloudflare, create a DNS `TXT` record to verify your domain
+- In Cloudflare, create a DNS `CNAME` record poiting `www` to `domain.com`
+
## SSL/TLS Certificates
Every GitLab Pages project on GitLab.com will be available under
diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png
deleted file mode 100644
index 2661a497b91..00000000000
--- a/doc/user/project/pages/img/dns_add_new_a_record_example_updated.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png b/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png
new file mode 100644
index 00000000000..fa72df66587
--- /dev/null
+++ b/doc/user/project/pages/img/dns_add_new_a_record_example_updated_2018.png
Binary files differ
diff --git a/doc/user/project/repository/img/repository_languages.png b/doc/user/project/repository/img/repository_languages.png
new file mode 100644
index 00000000000..d9fb1278e06
--- /dev/null
+++ b/doc/user/project/repository/img/repository_languages.png
Binary files differ
diff --git a/doc/user/project/repository/index.md b/doc/user/project/repository/index.md
index 704c1777e62..1c3915a5fdd 100644
--- a/doc/user/project/repository/index.md
+++ b/doc/user/project/repository/index.md
@@ -155,6 +155,16 @@ The repository graph displays visually the Git flow strategy used in that reposi
Find it under your project's **Repository > Graph**.
+## Repository Languages
+
+For the default branch of each repository, GitLab will determine what programming languages
+were used and display this on the projects pages.
+
+![Repository Languages bar](img/repository_languages.png)
+
+Not all files are detected, among others; documentation,
+vendored code, and most markup languages are excluded.
+
## Compare
Select branches to compare and view the changes inline:
diff --git a/lib/gitlab/git/repository.rb b/lib/gitlab/git/repository.rb
index e9c901f8592..9521a2d63a0 100644
--- a/lib/gitlab/git/repository.rb
+++ b/lib/gitlab/git/repository.rb
@@ -543,14 +543,8 @@ module Gitlab
end
def update_branch(branch_name, user:, newrev:, oldrev:)
- gitaly_migrate(:operation_user_update_branch) do |is_enabled|
- if is_enabled
- gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
- else
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- OperationService.new(user, self).update_branch(branch_name, newrev, oldrev)
- end
- end
+ wrapped_gitaly_errors do
+ gitaly_operation_client.user_update_branch(branch_name, user, newrev, oldrev)
end
end
diff --git a/lib/gitlab/github_import/bulk_importing.rb b/lib/gitlab/github_import/bulk_importing.rb
index 147597289cf..da2f96b5c4b 100644
--- a/lib/gitlab/github_import/bulk_importing.rb
+++ b/lib/gitlab/github_import/bulk_importing.rb
@@ -15,10 +15,12 @@ module Gitlab
end
# Bulk inserts the given rows into the database.
- def bulk_insert(model, rows, batch_size: 100)
+ def bulk_insert(model, rows, batch_size: 100, pre_hook: nil)
rows.each_slice(batch_size) do |slice|
+ pre_hook.call(slice) if pre_hook
Gitlab::Database.bulk_insert(model.table_name, slice)
end
+ rows
end
end
end
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 31fefebf787..ead4215810f 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -55,7 +55,11 @@ module Gitlab
updated_at: issue.updated_at
}
- GithubImport.insert_and_return_id(attributes, project.issues)
+ GithubImport.insert_and_return_id(attributes, project.issues).tap do |id|
+ # We use .insert_and_return_id which effectively disables all callbacks.
+ # Trigger iid logic here to make sure we track internal id values consistently.
+ project.issues.find(id).ensure_project_iid!
+ end
rescue ActiveRecord::InvalidForeignKey
# It's possible the project has been deleted since scheduling this
# job. In this case we'll just skip creating the issue.
diff --git a/lib/gitlab/github_import/importer/milestones_importer.rb b/lib/gitlab/github_import/importer/milestones_importer.rb
index c53480e828a..94eb9136b9a 100644
--- a/lib/gitlab/github_import/importer/milestones_importer.rb
+++ b/lib/gitlab/github_import/importer/milestones_importer.rb
@@ -17,10 +17,20 @@ module Gitlab
end
def execute
- bulk_insert(Milestone, build_milestones)
+ # We insert records in bulk, by-passing any standard model callbacks.
+ # The pre_hook here makes sure we track internal ids consistently.
+ # Note this has to be called before performing an insert of a batch
+ # because we're outside a transaction scope here.
+ bulk_insert(Milestone, build_milestones, pre_hook: method(:track_greatest_iid))
build_milestones_cache
end
+ def track_greatest_iid(slice)
+ greatest_iid = slice.max { |e| e[:iid] }[:iid]
+
+ InternalId.track_greatest(nil, { project: project }, :milestones, greatest_iid, ->(_) { project.milestones.maximum(:iid) })
+ end
+
def build_milestones
build_database_rows(each_milestone)
end
diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb
index 6b3688c4381..e4b49d2143a 100644
--- a/lib/gitlab/github_import/importer/pull_request_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -76,7 +76,13 @@ module Gitlab
merge_request_id = GithubImport
.insert_and_return_id(attributes, project.merge_requests)
- [project.merge_requests.find(merge_request_id), false]
+ merge_request = project.merge_requests.find(merge_request_id)
+
+ # We use .insert_and_return_id which effectively disables all callbacks.
+ # Trigger iid logic here to make sure we track internal id values consistently.
+ merge_request.ensure_target_project_iid!
+
+ [merge_request, false]
end
rescue ActiveRecord::InvalidForeignKey
# It's possible the project has been deleted since scheduling this
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index ac827cbe1ca..bcbaf00e11b 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -45,7 +45,7 @@ module Gitlab
end
def ensure_default_member!
- @project.project_members.destroy_all
+ @project.project_members.destroy_all # rubocop: disable DestroyAll
ProjectMember.create!(user: @user, access_level: ProjectMember::MAINTAINER, source_id: @project.id, importing: true)
end
diff --git a/lib/tasks/gitlab/site_statistics.rake b/lib/tasks/gitlab/site_statistics.rake
new file mode 100644
index 00000000000..7d24ec72a9d
--- /dev/null
+++ b/lib/tasks/gitlab/site_statistics.rake
@@ -0,0 +1,23 @@
+namespace :gitlab do
+ desc "GitLab | Refresh Site Statistics counters"
+ task refresh_site_statistics: :environment do
+ puts 'Updating Site Statistics counters: '
+
+ print '* Repositories... '
+ SiteStatistic.transaction do
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+ ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql?
+ SiteStatistic.update_all('repositories_count = (SELECT COUNT(*) FROM projects)')
+ end
+ puts 'OK!'.color(:green)
+
+ print '* Wikis... '
+ SiteStatistic.transaction do
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+ ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql?
+ SiteStatistic.update_all('wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)')
+ end
+ puts 'OK!'.color(:green)
+ puts
+ end
+end
diff --git a/lib/tasks/gitlab/traces.rake b/lib/tasks/gitlab/traces.rake
index ddcca69711f..5a232091a7e 100644
--- a/lib/tasks/gitlab/traces.rake
+++ b/lib/tasks/gitlab/traces.rake
@@ -18,5 +18,22 @@ namespace :gitlab do
logger.info("Scheduled #{job_ids.count} jobs. From #{job_ids.min} to #{job_ids.max}")
end
end
+
+ task migrate: :environment do
+ logger = Logger.new(STDOUT)
+ logger.info('Starting transfer of job traces')
+
+ Ci::Build.joins(:project)
+ .with_archived_trace_stored_locally
+ .find_each(batch_size: 10) do |build|
+ begin
+ build.job_artifacts_trace.file.migrate!(ObjectStorage::Store::REMOTE)
+
+ logger.info("Transferred job trace of #{build.id} to object storage")
+ rescue => e
+ logger.error("Failed to transfer artifacts of #{build.id} with error: #{e.message}")
+ end
+ end
+ end
end
end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b01a0068694..73bff79aabe 100644
--- a/locale/gitlab.pot
+++ b/locale/gitlab.pot
@@ -235,6 +235,9 @@ msgstr ""
msgid "<code>\"johnsmith@example.com\": \"johnsmith@example.com\"</code> will add \"By <a href=\"#\">johnsmith@example.com</a>\" to all issues and comments originally created by johnsmith@example.com. By default, the email address or username is masked to ensure the user's privacy. Use this option if you want to show the full email address."
msgstr ""
+msgid "<strong>%{changedFilesLength} unstaged</strong> and <strong>%{stagedFilesLength} staged</strong> changes"
+msgstr ""
+
msgid "<strong>%{group_name}</strong> group members"
msgstr ""
@@ -346,6 +349,12 @@ msgstr ""
msgid "Admin area"
msgstr ""
+msgid "AdminArea| You are about to permanently delete the user %{username}. Issues, merge requests, and groups linked to them will be transferred to a system-wide \"Ghost-user\". To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "AdminArea| You are about to permanently delete the user %{username}. This will delete all of the issues, merge requests, and groups linked to them. To avoid data loss, consider using the %{strong_start}block user%{strong_end} feature instead. Once you %{strong_start}Delete user%{strong_end}, it cannot be undone or recovered."
+msgstr ""
+
msgid "AdminArea|Stop all jobs"
msgstr ""
@@ -364,6 +373,9 @@ msgstr ""
msgid "AdminHealthPageLink|health page"
msgstr ""
+msgid "AdminProjects| You’re about to permanently delete the project %{projectName}, its repository, and all related resources including issues, merge requests, etc.. Once you confirm and press %{strong_start}Delete project%{strong_end}, it cannot be undone or recovered."
+msgstr ""
+
msgid "AdminProjects|Delete"
msgstr ""
@@ -499,7 +511,7 @@ msgstr ""
msgid "An error occurred while getting projects"
msgstr ""
-msgid "An error occurred while importing project: ${details}"
+msgid "An error occurred while importing project: %{details}"
msgstr ""
msgid "An error occurred while loading commit signatures"
@@ -805,6 +817,9 @@ msgstr ""
msgid "Badges|This project has no badges"
msgstr ""
+msgid "Badges|You are going to delete this badge. Deleted badges <strong>cannot</strong> be restored."
+msgstr ""
+
msgid "Badges|Your badges"
msgstr ""
@@ -1248,6 +1263,9 @@ msgstr ""
msgid "ClusterIntegration|%{appList} was successfully installed on your Kubernetes cluster"
msgstr ""
+msgid "ClusterIntegration|%{boldNotice} This will add some extra resources like a load balancer, which may incur additional costs depending on the hosting provider your Kubernetes cluster is installed on. If you are using Google Kubernetes Engine, you can %{pricingLink}."
+msgstr ""
+
msgid "ClusterIntegration|API URL"
msgstr ""
@@ -1257,12 +1275,21 @@ msgstr ""
msgid "ClusterIntegration|Advanced options on this Kubernetes cluster's integration"
msgstr ""
+msgid "ClusterIntegration|After installing Ingress, you will need to point your wildcard DNS at the generated external IP address in order to view your app after it is deployed. %{ingressHelpLink}"
+msgstr ""
+
msgid "ClusterIntegration|An error occured while trying to fetch project zones: %{error}"
msgstr ""
msgid "ClusterIntegration|An error occured while trying to fetch your projects: %{error}"
msgstr ""
+msgid "ClusterIntegration|An error occured while trying to fetch zone machine types: %{error}"
+msgstr ""
+
+msgid "ClusterIntegration|An error occurred when trying to contact the Google Cloud API. Please try again later."
+msgstr ""
+
msgid "ClusterIntegration|Applications"
msgstr ""
@@ -1329,6 +1356,9 @@ msgstr ""
msgid "ClusterIntegration|GitLab Runner"
msgstr ""
+msgid "ClusterIntegration|GitLab Runner connects to this project's repository and executes CI/CD jobs, pushing results back and deploying, applications to production."
+msgstr ""
+
msgid "ClusterIntegration|Google Cloud Platform project"
msgstr ""
@@ -1341,6 +1371,9 @@ msgstr ""
msgid "ClusterIntegration|Helm Tiller"
msgstr ""
+msgid "ClusterIntegration|Helm streamlines installing and managing Kubernetes applications. Tiller runs inside of your Kubernetes Cluster, and manages releases of your charts."
+msgstr ""
+
msgid "ClusterIntegration|Hide"
msgstr ""
@@ -1350,9 +1383,15 @@ msgstr ""
msgid "ClusterIntegration|Ingress IP Address"
msgstr ""
+msgid "ClusterIntegration|Ingress gives you a way to route requests to services based on the request host or path, centralizing a number of services into a single entrypoint."
+msgstr ""
+
msgid "ClusterIntegration|Install"
msgstr ""
+msgid "ClusterIntegration|Install applications on your Kubernetes cluster. Read more about %{helpLink}"
+msgstr ""
+
msgid "ClusterIntegration|Installed"
msgstr ""
@@ -1371,6 +1410,9 @@ msgstr ""
msgid "ClusterIntegration|JupyterHub"
msgstr ""
+msgid "ClusterIntegration|JupyterHub, a multi-user Hub, spawns, manages, and proxies multiple instances of the single-user Jupyter notebook server. JupyterHub can be used to serve notebooks to a class of students, a corporate data science group, or a scientific research group."
+msgstr ""
+
msgid "ClusterIntegration|Kubernetes cluster"
msgstr ""
@@ -1458,6 +1500,9 @@ msgstr ""
msgid "ClusterIntegration|Please make sure that your Google account meets the following requirements:"
msgstr ""
+msgid "ClusterIntegration|Point a wildcard DNS to this generated IP address in order to access your application after it has been deployed."
+msgstr ""
+
msgid "ClusterIntegration|Project namespace"
msgstr ""
@@ -1467,6 +1512,9 @@ msgstr ""
msgid "ClusterIntegration|Prometheus"
msgstr ""
+msgid "ClusterIntegration|Prometheus is an open-source monitoring system with %{gitlabIntegrationLink} to monitor deployed applications."
+msgstr ""
+
msgid "ClusterIntegration|Read our %{link_to_help_page} on Kubernetes cluster integration."
msgstr ""
@@ -1479,6 +1527,9 @@ msgstr ""
msgid "ClusterIntegration|Remove this Kubernetes cluster's configuration from this project. This will not delete your actual Kubernetes cluster."
msgstr ""
+msgid "ClusterIntegration|Replace this with your own hostname if you want. If you do so, point hostname to Ingress IP Address from above."
+msgstr ""
+
msgid "ClusterIntegration|Request to begin installing failed"
msgstr ""
@@ -1533,6 +1584,9 @@ msgstr ""
msgid "ClusterIntegration|Something went wrong while installing %{title}"
msgstr ""
+msgid "ClusterIntegration|The IP address is in the process of being assigned. Please check your Kubernetes cluster or Quotas on Google Kubernetes Engine if it takes a long time."
+msgstr ""
+
msgid "ClusterIntegration|The default cluster configuration grants access to a wide set of functionalities needed to successfully build and deploy a containerised application."
msgstr ""
@@ -1551,6 +1605,9 @@ msgstr ""
msgid "ClusterIntegration|Validating project billing status"
msgstr ""
+msgid "ClusterIntegration|We could not verify that one of your projects on GCP has billing enabled. Please try again."
+msgstr ""
+
msgid "ClusterIntegration|With a Kubernetes cluster associated to this project, you can use review apps, deploy your applications, run your pipelines, and much more in an easy way."
msgstr ""
@@ -1963,6 +2020,9 @@ msgstr ""
msgid "Cycle Analytics"
msgstr ""
+msgid "Cycle Analytics gives an overview of how much time it takes to go from idea to production in your project."
+msgstr ""
+
msgid "CycleAnalyticsStage|Code"
msgstr ""
@@ -2361,6 +2421,9 @@ msgstr ""
msgid "Environments|Environments"
msgstr ""
+msgid "Environments|Environments are places where code gets deployed, such as staging or production."
+msgstr ""
+
msgid "Environments|Job"
msgstr ""
@@ -2373,6 +2436,9 @@ msgstr ""
msgid "Environments|No deployments yet"
msgstr ""
+msgid "Environments|Note that this action will stop the environment, but it will %{emphasisStart}not%{emphasisEnd} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ciConfigLinkStart}.gitlab-ci.yml%{ciConfigLinkEnd} file."
+msgstr ""
+
msgid "Environments|Note that this action will stop the environment, but it will %{emphasis_start}not%{emphasis_end} have an effect on any existing deployment due to no “stop environment action” being defined in the %{ci_config_link_start}.gitlab-ci.yml%{ci_config_link_end} file."
msgstr ""
@@ -3139,12 +3205,21 @@ msgstr ""
msgid "Jobs"
msgstr ""
+msgid "Job|Are you sure you want to erase this job?"
+msgstr ""
+
msgid "Job|Browse"
msgstr ""
+msgid "Job|Complete Raw"
+msgstr ""
+
msgid "Job|Download"
msgstr ""
+msgid "Job|Erase job log"
+msgstr ""
+
msgid "Job|Job artifacts"
msgstr ""
@@ -3157,12 +3232,24 @@ msgstr ""
msgid "Job|Keep"
msgstr ""
+msgid "Job|Scroll to bottom"
+msgstr ""
+
+msgid "Job|Scroll to top"
+msgstr ""
+
+msgid "Job|Show complete raw"
+msgstr ""
+
msgid "Job|The artifacts were removed"
msgstr ""
msgid "Job|The artifacts will be removed"
msgstr ""
+msgid "Job|This job is stuck, because the project doesn't have any runners online assigned to it."
+msgstr ""
+
msgid "Jul"
msgstr ""
@@ -3244,6 +3331,9 @@ msgstr ""
msgid "Labels|Promote Label"
msgstr ""
+msgid "Labels|Promoting %{labelTitle} will make it available for all projects inside %{groupName}. Existing project labels with the same title will be merged. This action cannot be reversed."
+msgstr ""
+
msgid "Last %d day"
msgid_plural "Last %d days"
msgstr[0] ""
@@ -3303,6 +3393,11 @@ msgstr ""
msgid "Leave the \"File type\" and \"Delivery method\" options on their default values."
msgstr ""
+msgid "Limited to showing %d event at most"
+msgid_plural "Limited to showing %d events at most"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "LinkedIn"
msgstr ""
@@ -3339,6 +3434,9 @@ msgstr ""
msgid "Lock not found"
msgstr ""
+msgid "Lock this %{issuableDisplayName}? Only <strong>project members</strong> will be able to comment."
+msgstr ""
+
msgid "Lock to current projects"
msgstr ""
@@ -3465,6 +3563,9 @@ msgstr ""
msgid "MergeRequests|View replaced file @ %{commitId}"
msgstr ""
+msgid "MergeRequest| %{paragraphStart}changed the description %{descriptionChangedTimes} times %{timeDifferenceMinutes}%{paragraphEnd}"
+msgstr ""
+
msgid "Merged"
msgstr ""
@@ -3513,6 +3614,12 @@ msgstr ""
msgid "Milestones"
msgstr ""
+msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project and remove it from %{issuesWithCount} and %{mergeRequestsWithCount}. Once deleted, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Milestones| You’re about to permanently delete the milestone %{milestoneTitle} from this project. %{milestoneTitle} is not currently used in any issues or merge requests."
+msgstr ""
+
msgid "Milestones|Delete milestone"
msgstr ""
@@ -3531,6 +3638,9 @@ msgstr ""
msgid "Milestones|Promote Milestone"
msgstr ""
+msgid "Milestones|Promoting %{milestoneTitle} will make it available for all projects inside %{groupName}. Existing project milestones with the same title will be merged. This action cannot be reversed."
+msgstr ""
+
msgid "Mirror a repository"
msgstr ""
@@ -3701,6 +3811,9 @@ msgstr ""
msgid "No connection could be made to a Gitaly Server, please check your logs!"
msgstr ""
+msgid "No container images stored for this project. Add one by following the instructions above."
+msgstr ""
+
msgid "No due date"
msgstr ""
@@ -3746,6 +3859,12 @@ msgstr ""
msgid "None"
msgstr ""
+msgid "Not all comments are displayed because you're comparing two versions of the diff."
+msgstr ""
+
+msgid "Not all comments are displayed because you're viewing an old version of the diff."
+msgstr ""
+
msgid "Not allowed to merge"
msgstr ""
@@ -3857,6 +3976,11 @@ msgstr ""
msgid "OfSearchInADropdown|Filter"
msgstr ""
+msgid "One more item"
+msgid_plural "%d more items"
+msgstr[0] ""
+msgstr[1] ""
+
msgid "One or more of your Bitbucket projects cannot be imported into GitLab directly because they use Subversion or Mercurial for version control, rather than Git."
msgstr ""
@@ -3965,6 +4089,9 @@ msgstr ""
msgid "Pending"
msgstr ""
+msgid "People without permission will never get a notification and won't be able to comment."
+msgstr ""
+
msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
@@ -3983,6 +4110,9 @@ msgstr ""
msgid "Pipeline"
msgstr ""
+msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
+msgstr ""
+
msgid "Pipeline Health"
msgstr ""
@@ -4067,6 +4197,9 @@ msgstr ""
msgid "Pipelines|Clear Runner Caches"
msgstr ""
+msgid "Pipelines|Continuous Integration can help catch bugs by running your tests automatically, while Continuous Deployment can help you deliver code to your product environment."
+msgstr ""
+
msgid "Pipelines|Get started with Pipelines"
msgstr ""
@@ -4088,6 +4221,9 @@ msgstr ""
msgid "Pipelines|There are currently no pipelines."
msgstr ""
+msgid "Pipelines|There was an error fetching the pipelines. Try again in a few moments or contact your support team."
+msgstr ""
+
msgid "Pipelines|This project is not currently set up to run pipelines."
msgstr ""
@@ -4202,6 +4338,12 @@ msgstr ""
msgid "Profile Settings"
msgstr ""
+msgid "Profiles| You are about to permanently delete %{yourAccount}, and all of the issues, merge requests, and groups linked to your account. Once you confirm %{deleteAccount}, it cannot be undone or recovered."
+msgstr ""
+
+msgid "Profiles| You are going to change the username %{currentUsernameBold} to %{newUsernameBold}. Profile and projects will be redirected to the %{newUsername} namespace but this redirect will expire once the %{currentUsername} namespace is registered by another user or group. Please update your Git repository remotes as soon as possible."
+msgstr ""
+
msgid "Profiles|Account scheduled for removal."
msgstr ""
@@ -4376,6 +4518,9 @@ msgstr ""
msgid "ProjectsDropdown|Sorry, no projects matched your search"
msgstr ""
+msgid "ProjectsDropdown|This feature requires browser localStorage support"
+msgstr ""
+
msgid "PrometheusDashboard|Time"
msgstr ""
@@ -4502,6 +4647,11 @@ msgstr ""
msgid "Reference:"
msgstr ""
+msgid "Refreshing in a second to show the updated status..."
+msgid_plural "Refreshing in %d seconds to show the updated status..."
+msgstr[0] ""
+msgstr[1] ""
+
msgid "Register / Sign In"
msgstr ""
@@ -4649,6 +4799,9 @@ msgstr ""
msgid "Retry verification"
msgstr ""
+msgid "Reveal Variables"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -4965,6 +5118,12 @@ msgstr ""
msgid "Something went wrong on our end. Please try again!"
msgstr ""
+msgid "Something went wrong trying to change the confidentiality of this issue"
+msgstr ""
+
+msgid "Something went wrong trying to change the locked state of this %{issuableDisplayName}"
+msgstr ""
+
msgid "Something went wrong when toggling the button"
msgstr ""
@@ -5303,6 +5462,9 @@ msgstr ""
msgid "The Issue Tracker is the place to add things that need to be improved or solved in a project. You can register or sign in to create issues for this project."
msgstr ""
+msgid "The character highlighter helps you keep the subject line to %{titleLength} characters and wrap the body at %{bodyLength} so they are readable in git."
+msgstr ""
+
msgid "The coding stage shows the time from the first commit to creating the merge request. The data will automatically be added here once you create your first merge request."
msgstr ""
@@ -5333,6 +5495,9 @@ msgstr ""
msgid "The phase of the development lifecycle."
msgstr ""
+msgid "The pipelines schedule runs pipelines in the future, repeatedly, for specific branches or tags. Those scheduled pipelines will inherit limited project access based on their associated user."
+msgstr ""
+
msgid "The planning stage shows the time from the previous step to pushing your first commit. This time will be added automatically once you push your first commit."
msgstr ""
@@ -5435,6 +5600,9 @@ msgstr ""
msgid "This application will be able to:"
msgstr ""
+msgid "This branch has changed since you started editing. Would you like to create a new branch?"
+msgstr ""
+
msgid "This diff is collapsed."
msgstr ""
@@ -5486,6 +5654,12 @@ msgstr ""
msgid "This job is in pending state and is waiting to be picked by a runner"
msgstr ""
+msgid "This job is stuck, because you don't have any active runners online with any of these tags assigned to them:"
+msgstr ""
+
+msgid "This job is stuck, because you don't have any active runners that can run this job."
+msgstr ""
+
msgid "This job requires a manual action"
msgstr ""
@@ -5495,6 +5669,9 @@ msgstr ""
msgid "This merge request is locked."
msgstr ""
+msgid "This option is disabled as you don't have write permissions for the current branch"
+msgstr ""
+
msgid "This option is disabled while you still have unstaged changes"
msgstr ""
@@ -5510,6 +5687,9 @@ msgstr ""
msgid "This project does not belong to a group and can therefore not make use of group Runners."
msgstr ""
+msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
+msgstr ""
+
msgid "This repository"
msgstr ""
@@ -5784,6 +5964,9 @@ msgstr ""
msgid "Trending"
msgstr ""
+msgid "Trigger"
+msgstr ""
+
msgid "Trigger this manual action"
msgstr ""
@@ -5802,6 +5985,9 @@ msgstr ""
msgid "Unlock"
msgstr ""
+msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
+msgstr ""
+
msgid "Unlocked"
msgstr ""
@@ -6315,6 +6501,12 @@ msgstr ""
msgid "command line instructions"
msgstr ""
+msgid "confidentiality|You are going to turn off the confidentiality. This means <strong>everyone</strong> will be able to see and leave a comment on this issue."
+msgstr ""
+
+msgid "confidentiality|You are going to turn on the confidentiality. This means that only team members with <strong>at least Reporter access</strong> are able to see and leave comments on the issue."
+msgstr ""
+
msgid "connecting"
msgstr ""
@@ -6418,6 +6610,9 @@ msgstr ""
msgid "mrWidget|Failed to load deployment statistics"
msgstr ""
+msgid "mrWidget|Fast-forward merge is not possible. To merge this request, first rebase locally."
+msgstr ""
+
msgid "mrWidget|If the %{branch} branch exists in your local repository, you can merge this merge request manually using the"
msgstr ""
@@ -6445,9 +6640,15 @@ msgstr ""
msgid "mrWidget|Open in Web IDE"
msgstr ""
+msgid "mrWidget|Pipeline blocked. The pipeline for this merge request requires a manual action to proceed"
+msgstr ""
+
msgid "mrWidget|Plain diff"
msgstr ""
+msgid "mrWidget|Ready to be merged automatically. Ask someone with write access to this repository to merge this request"
+msgstr ""
+
msgid "mrWidget|Refresh"
msgstr ""
@@ -6469,6 +6670,9 @@ msgstr ""
msgid "mrWidget|Resolve conflicts"
msgstr ""
+msgid "mrWidget|Resolve these conflicts or ask someone with write access to this repository to merge it locally"
+msgstr ""
+
msgid "mrWidget|Revert"
msgstr ""
@@ -6487,6 +6691,12 @@ msgstr ""
msgid "mrWidget|The changes will be merged into"
msgstr ""
+msgid "mrWidget|The pipeline for this merge request failed. Please retry the job or push a new commit to fix the failure"
+msgstr ""
+
+msgid "mrWidget|The source branch HEAD has recently changed. Please reload the page and review the changes before merging"
+msgstr ""
+
msgid "mrWidget|The source branch has been removed"
msgstr ""
diff --git a/package.json b/package.json
index 975dd2619d7..f5fc759f76e 100644
--- a/package.json
+++ b/package.json
@@ -126,6 +126,8 @@
"eslint-plugin-jasmine": "^2.1.0",
"eslint-plugin-promise": "^3.8.0",
"eslint-plugin-vue": "^4.5.0",
+ "gettext-extractor": "^3.3.2",
+ "gettext-extractor-vue": "^4.0.1",
"ignore": "^3.3.7",
"istanbul": "^0.4.5",
"jasmine-core": "^2.9.0",
diff --git a/rubocop/cop/destroy_all.rb b/rubocop/cop/destroy_all.rb
new file mode 100644
index 00000000000..38b6cb40f91
--- /dev/null
+++ b/rubocop/cop/destroy_all.rb
@@ -0,0 +1,22 @@
+# frozen_string_literal: true
+
+module RuboCop
+ module Cop
+ # Cop that blacklists the use of `destroy_all`.
+ class DestroyAll < RuboCop::Cop::Cop
+ MSG = 'Use `delete_all` instead of `destroy_all`. ' \
+ '`destroy_all` will load the rows into memory, then execute a ' \
+ '`DELETE` for every individual row.'
+
+ def_node_matcher :destroy_all?, <<~PATTERN
+ (send {send ivar lvar const} :destroy_all ...)
+ PATTERN
+
+ def on_send(node)
+ return unless destroy_all?(node)
+
+ add_offense(node, location: :expression)
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index a427208cdab..88c9bbf24f4 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -27,3 +27,4 @@ require_relative 'cop/project_path_helper'
require_relative 'cop/rspec/env_assignment'
require_relative 'cop/rspec/factories_in_migration_specs'
require_relative 'cop/sidekiq_options_queue'
+require_relative 'cop/destroy_all'
diff --git a/scripts/frontend/extract_gettext_all.js b/scripts/frontend/extract_gettext_all.js
new file mode 100644
index 00000000000..af8cc4c3341
--- /dev/null
+++ b/scripts/frontend/extract_gettext_all.js
@@ -0,0 +1,72 @@
+const argumentsParser = require('commander');
+
+const { GettextExtractor, JsExtractors } = require('gettext-extractor');
+const {
+ decorateJSParserWithVueSupport,
+ decorateExtractorWithHelpers,
+} = require('gettext-extractor-vue');
+const ensureSingleLine = require('../../app/assets/javascripts/locale/ensure_single_line.js');
+
+const arguments = argumentsParser
+ .option('-f, --file <file>', 'Extract message from one single file')
+ .option('-a, --all', 'Extract message from all js/vue files')
+ .parse(process.argv);
+
+const extractor = decorateExtractorWithHelpers(new GettextExtractor());
+
+extractor.addMessageTransformFunction(ensureSingleLine);
+
+const jsParser = extractor.createJsParser([
+ // Place all the possible expressions to extract here:
+ JsExtractors.callExpression('__', {
+ arguments: {
+ text: 0,
+ },
+ }),
+ JsExtractors.callExpression('n__', {
+ arguments: {
+ text: 0,
+ textPlural: 1,
+ },
+ }),
+ JsExtractors.callExpression('s__', {
+ arguments: {
+ text: 0,
+ },
+ }),
+]);
+
+const vueParser = decorateJSParserWithVueSupport(jsParser);
+
+function printJson() {
+ const messages = extractor.getMessages().reduce((result, message) => {
+ let text = message.text;
+ if (message.textPlural) {
+ text += `\u0000${message.textPlural}`;
+ }
+
+ message.references.forEach(reference => {
+ const filename = reference.replace(/:\d+$/, '');
+
+ if (!Array.isArray(result[filename])) {
+ result[filename] = [];
+ }
+
+ result[filename].push([text, reference]);
+ });
+
+ return result;
+ }, {});
+
+ console.log(JSON.stringify(messages));
+}
+
+if (arguments.file) {
+ vueParser.parseFile(arguments.file).then(() => printJson());
+} else if (arguments.all) {
+ vueParser.parseFilesGlob('{ee/app,app}/assets/javascripts/**/*.{js,vue}').then(() => printJson());
+} else {
+ console.warn('ERROR: Please use the script correctly:');
+ arguments.outputHelp();
+ process.exit(1);
+}
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index b23f183fec8..d377d69457f 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -95,7 +95,7 @@ describe OmniauthCallbacksController, type: :controller do
end
it 'allows linking the disabled provider' do
- user.identities.destroy_all
+ user.identities.destroy_all # rubocop: disable DestroyAll
sign_in(user)
expect { post provider }.to change { user.reload.identities.count }.by(1)
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index fc1619acec6..20a6beb3df8 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::ReleasesController do
describe 'GET #edit' do
it 'initializes a new release' do
tag_id = release.tag
- project.releases.destroy_all
+ project.releases.destroy_all # rubocop: disable DestroyAll
get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id
diff --git a/spec/features/instance_statistics/cohorts_spec.rb b/spec/features/instance_statistics/cohorts_spec.rb
index 81fc5eff980..573f8600be1 100644
--- a/spec/features/instance_statistics/cohorts_spec.rb
+++ b/spec/features/instance_statistics/cohorts_spec.rb
@@ -12,4 +12,12 @@ describe 'Cohorts page' do
expect(page).to have_content("#{Time.now.strftime('%b %Y')} 3 0")
end
+
+ it 'shows usage data', :js do
+ visit instance_statistics_cohorts_path
+
+ wait_for_requests
+
+ expect(find('.js-syntax-highlight').text).not_to eq('')
+ end
end
diff --git a/spec/features/instance_statistics/conversational_development_index_spec.rb b/spec/features/instance_statistics/conversational_development_index_spec.rb
index d441a7a5af9..a6c16b6a2a3 100644
--- a/spec/features/instance_statistics/conversational_development_index_spec.rb
+++ b/spec/features/instance_statistics/conversational_development_index_spec.rb
@@ -5,6 +5,16 @@ describe 'Conversational Development Index' do
sign_in(create(:admin))
end
+ it 'has dismissable intro callout', :js do
+ visit instance_statistics_conversational_development_index_index_path
+
+ expect(page).to have_content 'Introducing Your Conversational Development Index'
+
+ find('.js-close-callout').click
+
+ expect(page).not_to have_content 'Introducing Your Conversational Development Index'
+ end
+
context 'when usage ping is disabled' do
it 'shows empty state' do
stub_application_setting(usage_ping_enabled: false)
diff --git a/spec/javascripts/boards/board_list_spec.js b/spec/javascripts/boards/board_list_spec.js
index de261d36c61..290600cf995 100644
--- a/spec/javascripts/boards/board_list_spec.js
+++ b/spec/javascripts/boards/board_list_spec.js
@@ -200,6 +200,15 @@ describe('Board list component', () => {
});
});
+ it('does not load issues if already loading', () => {
+ component.list.nextPage = spyOn(component.list, 'nextPage').and.returnValue(new Promise(() => {}));
+
+ component.onScroll();
+ component.onScroll();
+
+ expect(component.list.nextPage).toHaveBeenCalledTimes(1);
+ });
+
it('shows loading more spinner', (done) => {
component.showCount = true;
component.list.loadingMore = true;
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 72eb20bdc87..bca2033ff97 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -352,10 +352,22 @@ describe('IDE store file actions', () => {
it('calls also getBaseRawFileData service method', done => {
spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+ store.state.currentProjectId = 'gitlab-org/gitlab-ce';
+ store.state.currentMergeRequestId = '1';
+ store.state.projects = {
+ 'gitlab-org/gitlab-ce': {
+ mergeRequests: {
+ 1: {
+ baseCommitSha: 'SHA',
+ },
+ },
+ },
+ };
+
tmpFile.mrChange = { new_file: false };
store
- .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
expect(tmpFile.baseRaw).toBe('baseraw');
@@ -392,10 +404,7 @@ describe('IDE store file actions', () => {
const dispatch = jasmine.createSpy('dispatch');
actions
- .getRawFileData(
- { state: store.state, commit() {}, dispatch },
- { path: tmpFile.path, baseSha: tmpFile.baseSha },
- )
+ .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path })
.then(done.fail)
.catch(() => {
expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
@@ -404,7 +413,6 @@ describe('IDE store file actions', () => {
actionText: 'Please try again',
actionPayload: {
path: tmpFile.path,
- baseSha: tmpFile.baseSha,
},
});
diff --git a/spec/javascripts/jobs/commit_block_spec.js b/spec/javascripts/jobs/commit_block_spec.js
new file mode 100644
index 00000000000..f755a5042b5
--- /dev/null
+++ b/spec/javascripts/jobs/commit_block_spec.js
@@ -0,0 +1,73 @@
+import Vue from 'vue';
+import component from '~/jobs/components/commit_block.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Commit block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const props = {
+ pipelineShortSha: '1f0fb84f',
+ pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ mergeRequestReference: '!21244',
+ mergeRequestPath: 'merge_requests/21244',
+ gitCommitTitlte: 'Regenerate pot files',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('pipeline short sha', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+ });
+
+ it('renders pipeline short sha link', () => {
+ expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath);
+ expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha);
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha);
+ });
+ });
+
+ describe('with merge request', () => {
+ it('renders merge request link and reference', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath);
+ expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference);
+
+ });
+ });
+
+ describe('without merge request', () => {
+ it('does not render merge request', () => {
+ const copyProps = Object.assign({}, props);
+ delete copyProps.mergeRequestPath;
+ delete copyProps.mergeRequestReference;
+
+ vm = mountComponent(Component, {
+ ...copyProps,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
+ });
+ });
+
+ describe('git commit title', () => {
+ it('renders git commit title', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.textContent).toContain(props.gitCommitTitlte);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/components/job_log_controllers_spec.js b/spec/javascripts/jobs/components/job_log_controllers_spec.js
new file mode 100644
index 00000000000..416dfab8a48
--- /dev/null
+++ b/spec/javascripts/jobs/components/job_log_controllers_spec.js
@@ -0,0 +1,217 @@
+import Vue from 'vue';
+import component from '~/jobs/components/job_log_controllers.vue';
+import mountComponent from '../../helpers/vue_mount_component_helper';
+
+describe('Job log controllers', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('Truncate information', () => {
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+ });
+
+ it('renders size information', () => {
+ expect(vm.$el.querySelector('.js-truncated-info').textContent).toContain('499.95 KiB');
+ });
+
+ it('renders link to raw trace', () => {
+ expect(vm.$el.querySelector('.js-raw-link').getAttribute('href')).toEqual('/raw');
+ });
+
+ });
+
+ describe('links section', () => {
+ describe('with raw trace path', () => {
+ it('renders raw trace link', () => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+
+ expect(vm.$el.querySelector('.js-raw-link-controller').getAttribute('href')).toEqual('/raw');
+ });
+ });
+
+ describe('without raw trace path', () => {
+ it('does not render raw trace link', () => {
+ vm = mountComponent(Component, {
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+
+ expect(vm.$el.querySelector('.js-raw-link-controller')).toBeNull();
+ });
+ });
+
+ describe('when is erasable', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+ });
+
+ it('renders erase job button', () => {
+ expect(vm.$el.querySelector('.js-erase-link')).not.toBeNull();
+ });
+
+ describe('on click', () => {
+ describe('when user confirms action', () => {
+ it('emits eraseJob event', () => {
+ spyOn(window, 'confirm').and.returnValue(true);
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-erase-link').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('eraseJob');
+ });
+ });
+
+ describe('when user does not confirm action', () => {
+ it('does not emit eraseJob event', () => {
+ spyOn(window, 'confirm').and.returnValue(false);
+ spyOn(vm, '$emit');
+
+ vm.$el.querySelector('.js-erase-link').click();
+
+ expect(vm.$emit).not.toHaveBeenCalledWith('eraseJob');
+ });
+ });
+ });
+ });
+
+ describe('when it is not erasable', () => {
+ it('does not render erase button', () => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: false,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+
+ expect(vm.$el.querySelector('.js-erase-link')).toBeNull();
+ });
+ });
+ });
+
+ describe('scroll buttons', () => {
+ describe('scroll top button', () => {
+ describe('when user can scroll top', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+ });
+
+ it('renders enabled scroll top button', () => {
+ expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toBeNull();
+ });
+
+ it('emits scrollJobLogTop event on click', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('.js-scroll-top').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogTop');
+ });
+ });
+
+ describe('when user can not scroll top', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: false,
+ canScrollToBottom: true,
+ });
+ });
+
+ it('renders disabled scroll top button', () => {
+ expect(vm.$el.querySelector('.js-scroll-top').getAttribute('disabled')).toEqual('disabled');
+ });
+
+ it('does not emit scrollJobLogTop event on click', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('.js-scroll-top').click();
+
+ expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogTop');
+ });
+ });
+ });
+
+ describe('scroll bottom button', () => {
+ describe('when user can scroll bottom', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: true,
+ });
+ });
+
+ it('renders enabled scroll bottom button', () => {
+ expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toBeNull();
+ });
+
+ it('emits scrollJobLogBottom event on click', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('.js-scroll-bottom').click();
+
+ expect(vm.$emit).toHaveBeenCalledWith('scrollJobLogBottom');
+ });
+ });
+
+ describe('when user can not scroll bottom', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ rawTracePath: '/raw',
+ canEraseJob: true,
+ size: 511952,
+ canScrollToTop: true,
+ canScrollToBottom: false,
+ });
+ });
+
+ it('renders disabled scroll bottom button', () => {
+ expect(vm.$el.querySelector('.js-scroll-bottom').getAttribute('disabled')).toEqual('disabled');
+
+ });
+
+ it('does not emit scrollJobLogBottom event on click', () => {
+ spyOn(vm, '$emit');
+ vm.$el.querySelector('.js-scroll-bottom').click();
+
+ expect(vm.$emit).not.toHaveBeenCalledWith('scrollJobLogBottom');
+ });
+ });
+ });
+ });
+});
+
diff --git a/spec/javascripts/jobs/jobs_container_spec.js b/spec/javascripts/jobs/jobs_container_spec.js
new file mode 100644
index 00000000000..bf52e65cbc8
--- /dev/null
+++ b/spec/javascripts/jobs/jobs_container_spec.js
@@ -0,0 +1,126 @@
+import Vue from 'vue';
+import component from '~/jobs/components/jobs_container.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Artifacts block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const retried = {
+ status: {
+ details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ path: 'job/233432756',
+ id: '233432756',
+ tooltip: 'build - passed',
+ retried: true,
+ };
+
+ const active = {
+ name: 'test',
+ status: {
+ details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ path: 'job/2322756',
+ id: '2322756',
+ tooltip: 'build - passed',
+ active: true,
+ };
+
+ const job = {
+ name: 'build',
+ status: {
+ details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ path: 'job/232153',
+ id: '232153',
+ tooltip: 'build - passed',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders list of jobs', () => {
+ vm = mountComponent(Component, {
+ jobs: [job, retried, active],
+ });
+
+ expect(vm.$el.querySelectorAll('a').length).toEqual(3);
+ });
+
+ it('renders arrow right when job is active', () => {
+ vm = mountComponent(Component, {
+ jobs: [active],
+ });
+
+ expect(vm.$el.querySelector('a .js-arrow-right')).not.toBeNull();
+ });
+
+ it('does not render arrow right when job is not active', () => {
+ vm = mountComponent(Component, {
+ jobs: [job],
+ });
+
+ expect(vm.$el.querySelector('a .js-arrow-right')).toBeNull();
+ });
+
+ it('renders job name when present', () => {
+ vm = mountComponent(Component, {
+ jobs: [job],
+ });
+
+ expect(vm.$el.querySelector('a').textContent.trim()).toContain(job.name);
+ expect(vm.$el.querySelector('a').textContent.trim()).not.toContain(job.id);
+ });
+
+ it('renders job id when job name is not available', () => {
+ vm = mountComponent(Component, {
+ jobs: [retried],
+ });
+
+ expect(vm.$el.querySelector('a').textContent.trim()).toContain(retried.id);
+ });
+
+ it('links to the job page', () => {
+ vm = mountComponent(Component, {
+ jobs: [job],
+ });
+
+ expect(vm.$el.querySelector('a').getAttribute('href')).toEqual(job.path);
+ });
+
+ it('renders retry icon when job was retried', () => {
+ vm = mountComponent(Component, {
+ jobs: [retried],
+ });
+
+ expect(vm.$el.querySelector('.js-retry-icon')).not.toBeNull();
+ });
+
+ it('does not render retry icon when job was not retried', () => {
+ vm = mountComponent(Component, {
+ jobs: [job],
+ });
+
+ expect(vm.$el.querySelector('.js-retry-icon')).toBeNull();
+ });
+});
diff --git a/spec/javascripts/jobs/stages_dropdown_spec.js b/spec/javascripts/jobs/stages_dropdown_spec.js
new file mode 100644
index 00000000000..d3a5d48f56c
--- /dev/null
+++ b/spec/javascripts/jobs/stages_dropdown_spec.js
@@ -0,0 +1,63 @@
+import Vue from 'vue';
+import component from '~/jobs/components/stages_dropdown.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Artifacts block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ pipelineId: 28029444,
+ pipelinePath: 'pipeline/28029444',
+ pipelineRef: '50101-truncated-job-information',
+ pipelineRefPath: 'commits/50101-truncated-job-information',
+ stages: [
+ {
+ name: 'build',
+ },
+ {
+ name: 'test',
+ },
+ ],
+ pipelineStatus: {
+ details_path: '/gitlab-org/gitlab-ce/pipelines/28029444',
+ group: 'success',
+ has_details: true,
+ icon: 'status_success',
+ label: 'passed',
+ text: 'passed',
+ tooltip: 'passed',
+ },
+ });
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('renders pipeline status', () => {
+ expect(vm.$el.querySelector('.js-ci-status-icon-success')).not.toBeNull();
+ });
+
+ it('renders pipeline link', () => {
+ expect(vm.$el.querySelector('.js-pipeline-path').getAttribute('href')).toEqual(
+ 'pipeline/28029444',
+ );
+ });
+
+ it('renders dropdown with stages', () => {
+ expect(vm.$el.querySelector('.dropdown button').textContent).toContain('build');
+ });
+
+ it('updates selected stage on click', done => {
+ vm.$el.querySelectorAll('.stage-item')[1].click();
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.dropdown button').textContent).toContain('test');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+});
diff --git a/spec/javascripts/jobs/trigger_value_spec.js b/spec/javascripts/jobs/trigger_value_spec.js
new file mode 100644
index 00000000000..acf91510ed2
--- /dev/null
+++ b/spec/javascripts/jobs/trigger_value_spec.js
@@ -0,0 +1,66 @@
+import Vue from 'vue';
+import component from '~/jobs/components/trigger_block.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Trigger block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with short token', () => {
+ it('renders short token', () => {
+ vm = mountComponent(Component, {
+ shortToken: '0a666b2',
+ });
+
+ expect(vm.$el.querySelector('.js-short-token').textContent).toContain('0a666b2');
+ });
+ });
+
+ describe('without short token', () => {
+ it('does not render short token', () => {
+ vm = mountComponent(Component, {});
+
+ expect(vm.$el.querySelector('.js-short-token')).toBeNull();
+ });
+ });
+
+ describe('with variables', () => {
+ describe('reveal variables', () => {
+ it('reveals variables on click', done => {
+ vm = mountComponent(Component, {
+ variables: {
+ key: 'value',
+ variable: 'foo',
+ },
+ });
+
+ vm.$el.querySelector('.js-reveal-variables').click();
+
+ vm
+ .$nextTick()
+ .then(() => {
+ expect(vm.$el.querySelector('.js-build-variables')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('key');
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('value');
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('variable');
+ expect(vm.$el.querySelector('.js-build-variables').textContent).toContain('foo');
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+ });
+
+ describe('without variables', () => {
+ it('does not render variables', () => {
+ vm = mountComponent(Component);
+
+ expect(vm.$el.querySelector('.js-reveal-variables')).toBeNull();
+ expect(vm.$el.querySelector('.js-build-variables')).toBeNull();
+ });
+ });
+});
diff --git a/spec/javascripts/locale/ensure_single_line_spec.js b/spec/javascripts/locale/ensure_single_line_spec.js
new file mode 100644
index 00000000000..bbefa8f40f3
--- /dev/null
+++ b/spec/javascripts/locale/ensure_single_line_spec.js
@@ -0,0 +1,35 @@
+import ensureSingleLine from '~/locale/ensure_single_line';
+
+describe('locale', () => {
+ describe('ensureSingleLine', () => {
+ it('should remove newlines at the start of the string', () => {
+ const result = 'Test';
+ expect(ensureSingleLine(`\n${result}`)).toBe(result);
+ expect(ensureSingleLine(`\t\n\t${result}`)).toBe(result);
+ expect(ensureSingleLine(`\r\n${result}`)).toBe(result);
+ expect(ensureSingleLine(`\r\n ${result}`)).toBe(result);
+ expect(ensureSingleLine(`\r ${result}`)).toBe(result);
+ expect(ensureSingleLine(` \n ${result}`)).toBe(result);
+ });
+
+ it('should remove newlines at the end of the string', () => {
+ const result = 'Test';
+ expect(ensureSingleLine(`${result}\n`)).toBe(result);
+ expect(ensureSingleLine(`${result}\t\n\t`)).toBe(result);
+ expect(ensureSingleLine(`${result}\r\n`)).toBe(result);
+ expect(ensureSingleLine(`${result}\r`)).toBe(result);
+ expect(ensureSingleLine(`${result} \r`)).toBe(result);
+ expect(ensureSingleLine(`${result} \r\n `)).toBe(result);
+ });
+
+ it('should replace newlines in the middle of the string with a single space', () => {
+ const result = 'Test';
+ expect(ensureSingleLine(`${result}\n${result}`)).toBe(`${result} ${result}`);
+ expect(ensureSingleLine(`${result}\t\n\t${result}`)).toBe(`${result} ${result}`);
+ expect(ensureSingleLine(`${result}\r\n${result}`)).toBe(`${result} ${result}`);
+ expect(ensureSingleLine(`${result}\r${result}`)).toBe(`${result} ${result}`);
+ expect(ensureSingleLine(`${result} \r${result}`)).toBe(`${result} ${result}`);
+ expect(ensureSingleLine(`${result} \r\n ${result}`)).toBe(`${result} ${result}`);
+ });
+ });
+});
diff --git a/spec/javascripts/notes/mock_data.js b/spec/javascripts/notes/mock_data.js
index 67f6a9629d9..0423fcb6ec4 100644
--- a/spec/javascripts/notes/mock_data.js
+++ b/spec/javascripts/notes/mock_data.js
@@ -1104,7 +1104,7 @@ export const collapsedSystemNotes = [
resolvable: false,
noteable_iid: 12,
note: 'changed the description',
- note_html: '\n <p dir="auto">changed the description 2 times within 1 minute </p>',
+ note_html: ' <p dir="auto">changed the description 2 times within 1 minute </p>',
current_user: { can_edit: false, can_award_emoji: true },
resolved: false,
resolved_by: null,
diff --git a/spec/javascripts/vue_shared/translate_spec.js b/spec/javascripts/vue_shared/translate_spec.js
index cbb3cbdff46..adb5ff682f0 100644
--- a/spec/javascripts/vue_shared/translate_spec.js
+++ b/spec/javascripts/vue_shared/translate_spec.js
@@ -1,88 +1,249 @@
import Vue from 'vue';
-import Translate from '~/vue_shared/translate';
+import Jed from 'jed';
-Vue.use(Translate);
+import locale from '~/locale';
+import Translate from '~/vue_shared/translate';
+import { trimText } from 'spec/helpers/vue_component_helper';
describe('Vue translate filter', () => {
let el;
+ const createTranslationMock = (key, ...translations) => {
+ const fakeLocale = new Jed({
+ domain: 'app',
+ locale_data: {
+ app: {
+ '': {
+ domain: 'app',
+ lang: 'vo',
+ plural_forms: 'nplurals=2; plural=(n != 1);',
+ },
+ [key]: translations,
+ },
+ },
+ });
+
+ // eslint-disable-next-line no-underscore-dangle
+ locale.__Rewire__('locale', fakeLocale);
+ };
+
+ afterEach(() => {
+ // eslint-disable-next-line no-underscore-dangle
+ locale.__ResetDependency__('locale');
+ });
+
beforeEach(() => {
+ Vue.use(Translate);
+
el = document.createElement('div');
document.body.appendChild(el);
});
- it('translate single text', (done) => {
- const comp = new Vue({
+ it('translate singular text (`__`)', done => {
+ const key = 'singular';
+ const translation = 'singular_translated';
+ createTranslationMock(key, translation);
+
+ const vm = new Vue({
+ el,
+ template: `
+ <span>
+ {{ __('${key}') }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(trimText(vm.$el.textContent)).toBe(translation);
+
+ done();
+ });
+ });
+
+ it('translate plural text (`n__`) without any substituting text', done => {
+ const key = 'plural';
+ const translationPlural = 'plural_multiple translation';
+ createTranslationMock(key, 'plural_singular translation', translationPlural);
+
+ const vm = new Vue({
el,
template: `
<span>
- {{ __('testing') }}
+ {{ n__('${key}', 'plurals', 2) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
- expect(
- comp.$el.textContent.trim(),
- ).toBe('testing');
+ expect(trimText(vm.$el.textContent)).toBe(translationPlural);
done();
});
});
- it('translate plural text with single count', (done) => {
- const comp = new Vue({
+ describe('translate plural text (`n__`) with substituting %d', () => {
+ const key = '%d day';
+
+ beforeEach(() => {
+ createTranslationMock(key, '%d singular translated', '%d plural translated');
+ });
+
+ it('and n === 1', done => {
+ const vm = new Vue({
+ el,
+ template: `
+ <span>
+ {{ n__('${key}', '%d days', 1) }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(trimText(vm.$el.textContent)).toBe('1 singular translated');
+
+ done();
+ });
+ });
+
+ it('and n > 1', done => {
+ const vm = new Vue({
+ el,
+ template: `
+ <span>
+ {{ n__('${key}', '%d days', 2) }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(trimText(vm.$el.textContent)).toBe('2 plural translated');
+
+ done();
+ });
+ });
+ });
+
+ describe('translates text with context `s__`', () => {
+ const key = 'Context|Foobar';
+ const translation = 'Context|Foobar translated';
+ const expectation = 'Foobar translated';
+
+ beforeEach(() => {
+ createTranslationMock(key, translation);
+ });
+
+ it('and using two parameters', done => {
+ const vm = new Vue({
+ el,
+ template: `
+ <span>
+ {{ s__('Context', 'Foobar') }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(trimText(vm.$el.textContent)).toBe(expectation);
+
+ done();
+ });
+ });
+
+ it('and using the pipe syntax', done => {
+ const vm = new Vue({
+ el,
+ template: `
+ <span>
+ {{ s__('${key}') }}
+ </span>
+ `,
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(trimText(vm.$el.textContent)).toBe(expectation);
+
+ done();
+ });
+ });
+ });
+
+ it('translate multi line text', done => {
+ const translation = 'multiline string translated';
+ createTranslationMock('multiline string', translation);
+
+ const vm = new Vue({
el,
template: `
<span>
- {{ n__('%d day', '%d days', 1) }}
+ {{ __(\`
+ multiline
+ string
+ \`) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
- expect(
- comp.$el.textContent.trim(),
- ).toBe('1 day');
+ expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
});
- it('translate plural text with multiple count', (done) => {
- const comp = new Vue({
+ it('translate pluralized multi line text', done => {
+ const translation = 'multiline string plural';
+
+ createTranslationMock('multiline string', 'multiline string singular', translation);
+
+ const vm = new Vue({
el,
template: `
<span>
- {{ n__('%d day', '%d days', 2) }}
+ {{ n__(
+ \`
+ multiline
+ string
+ \`,
+ \`
+ multiline
+ strings
+ \`,
+ 2
+ ) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
- expect(
- comp.$el.textContent.trim(),
- ).toBe('2 days');
+ expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
});
- it('translate plural without replacing any text', (done) => {
- const comp = new Vue({
+ it('translate pluralized multi line text with context', done => {
+ const translation = 'multiline string with context';
+
+ createTranslationMock('Context| multiline string', translation);
+
+ const vm = new Vue({
el,
template: `
<span>
- {{ n__('day', 'days', 2) }}
+ {{ s__(
+ \`
+ Context|
+ multiline
+ string
+ \`
+ ) }}
</span>
`,
}).$mount();
Vue.nextTick(() => {
- expect(
- comp.$el.textContent.trim(),
- ).toBe('days');
+ expect(trimText(vm.$el.textContent)).toBe(translation);
done();
});
diff --git a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
index 26d48cc8201..f92acf61682 100644
--- a/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
+++ b/spec/lib/gitlab/background_migration/create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -5,7 +5,7 @@ describe Gitlab::BackgroundMigration::CreateGpgKeySubkeysFromGpgKeys, :migration
let!(:gpg_key) { create(:gpg_key, key: GpgHelpers::User3.public_key) }
before do
- GpgKeySubkey.destroy_all
+ GpgKeySubkey.destroy_all # rubocop: disable DestroyAll
end
it 'generate the subkeys' do
diff --git a/spec/lib/gitlab/bare_repository_import/importer_spec.rb b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
index 6e21c846c0a..3c63e601abc 100644
--- a/spec/lib/gitlab/bare_repository_import/importer_spec.rb
+++ b/spec/lib/gitlab/bare_repository_import/importer_spec.rb
@@ -10,9 +10,6 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
subject(:importer) { described_class.new(admin, bare_repository) }
before do
- @rainbow = Rainbow.enabled
- Rainbow.enabled = false
-
allow(described_class).to receive(:log)
end
@@ -20,7 +17,6 @@ describe Gitlab::BareRepositoryImport::Importer, :seed_helper do
FileUtils.rm_rf(base_dir)
TestEnv.clean_test_path
ensure_seeds
- Rainbow.enabled = @rainbow
end
shared_examples 'importing a repository' do
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 35a6fc94753..17348b01006 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -542,7 +542,7 @@ describe Gitlab::Git::Repository, :seed_helper do
Gitlab::Shell.new.remove_repository('default', 'my_project')
end
- shared_examples 'repository mirror fecthing' do
+ shared_examples 'repository mirror fetching' do
it 'fetches a repository as a mirror remote' do
subject
@@ -569,11 +569,11 @@ describe Gitlab::Git::Repository, :seed_helper do
end
context 'with gitaly enabled' do
- it_behaves_like 'repository mirror fecthing'
+ it_behaves_like 'repository mirror fetching'
end
context 'with gitaly enabled', :skip_gitaly_mock do
- it_behaves_like 'repository mirror fecthing'
+ it_behaves_like 'repository mirror fetching'
end
def new_repository_path
diff --git a/spec/lib/gitlab/github_import/bulk_importing_spec.rb b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
index 91229d9c7d4..861710f7e9b 100644
--- a/spec/lib/gitlab/github_import/bulk_importing_spec.rb
+++ b/spec/lib/gitlab/github_import/bulk_importing_spec.rb
@@ -58,5 +58,17 @@ describe Gitlab::GithubImport::BulkImporting do
importer.bulk_insert(model, rows, batch_size: 5)
end
+
+ it 'calls pre_hook for each slice if given' do
+ rows = [{ title: 'Foo' }] * 10
+ model = double(:model, table_name: 'kittens')
+ pre_hook = double('pre_hook', call: nil)
+ allow(Gitlab::Database).to receive(:bulk_insert)
+
+ expect(pre_hook).to receive(:call).with(rows[0..4])
+ expect(pre_hook).to receive(:call).with(rows[5..9])
+
+ importer.bulk_insert(model, rows, batch_size: 5, pre_hook: pre_hook)
+ end
end
end
diff --git a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
index 81fe97c1e49..3f7a12144d5 100644
--- a/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/issue_importer_spec.rb
@@ -78,6 +78,11 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
.to receive(:id_for)
.with(issue)
.and_return(milestone.id)
+
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
end
context 'when the issue author could be found' do
@@ -172,6 +177,23 @@ describe Gitlab::GithubImport::Importer::IssueImporter, :clean_gitlab_redis_cach
expect(importer.create_issue).to be_a_kind_of(Numeric)
end
+
+ it 'triggers internal_id functionality to track greatest iids' do
+ allow(importer.user_finder)
+ .to receive(:author_id_for)
+ .with(issue)
+ .and_return([user.id, true])
+
+ issue = build_stubbed(:issue, project: project)
+ allow(Gitlab::GithubImport)
+ .to receive(:insert_and_return_id)
+ .and_return(issue.id)
+ allow(project.issues).to receive(:find).with(issue.id).and_return(issue)
+
+ expect(issue).to receive(:ensure_project_iid!)
+
+ importer.create_issue
+ end
end
describe '#create_assignees' do
diff --git a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
index b1cac3b6e46..db0be760c7b 100644
--- a/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb
@@ -29,13 +29,25 @@ describe Gitlab::GithubImport::Importer::MilestonesImporter, :clean_gitlab_redis
expect(importer)
.to receive(:bulk_insert)
- .with(Milestone, [milestone_hash])
+ .with(Milestone, [milestone_hash], any_args)
expect(importer)
.to receive(:build_milestones_cache)
importer.execute
end
+
+ it 'tracks internal ids' do
+ milestone_hash = { iid: 1, title: '1.0', project_id: project.id }
+ allow(importer)
+ .to receive(:build_milestones)
+ .and_return([milestone_hash])
+
+ expect(InternalId).to receive(:track_greatest)
+ .with(nil, { project: project }, :milestones, 1, any_args)
+
+ importer.execute
+ end
end
describe '#build_milestones' do
diff --git a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
index 3422a1e82fc..44c920043b4 100644
--- a/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
+++ b/spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb
@@ -111,6 +111,16 @@ describe Gitlab::GithubImport::Importer::PullRequestImporter, :clean_gitlab_redi
expect(mr).to be_instance_of(MergeRequest)
expect(exists).to eq(false)
end
+
+ it 'triggers internal_id functionality to track greatest iids' do
+ mr = build_stubbed(:merge_request, source_project: project, target_project: project)
+ allow(Gitlab::GithubImport).to receive(:insert_and_return_id).and_return(mr.id)
+ allow(project.merge_requests).to receive(:find).with(mr.id).and_return(mr)
+
+ expect(mr).to receive(:ensure_target_project_iid!)
+
+ importer.create_merge_request
+ end
end
context 'when the author could not be found' do
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
index 9da3648400e..e71e9da369d 100644
--- a/spec/lib/system_check/simple_executor_spec.rb
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -98,15 +98,6 @@ describe SystemCheck::SimpleExecutor do
end
end
- before do
- @rainbow = Rainbow.enabled
- Rainbow.enabled = false
- end
-
- after do
- Rainbow.enabled = @rainbow
- end
-
describe '#component' do
it 'returns stored component name' do
expect(subject.component).to eq('Test')
diff --git a/spec/migrations/delete_inconsistent_internal_id_records_spec.rb b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
new file mode 100644
index 00000000000..becb71cf427
--- /dev/null
+++ b/spec/migrations/delete_inconsistent_internal_id_records_spec.rb
@@ -0,0 +1,119 @@
+# frozen_string_literal: true
+# rubocop:disable RSpec/FactoriesInMigrationSpecs
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180723130817_delete_inconsistent_internal_id_records.rb')
+
+describe DeleteInconsistentInternalIdRecords, :migration do
+ let!(:project1) { create(:project) }
+ let!(:project2) { create(:project) }
+ let!(:project3) { create(:project) }
+
+ let(:internal_id_query) { ->(project) { InternalId.where(usage: InternalId.usages[scope.to_s.tableize], project: project) } }
+
+ let(:create_models) do
+ 3.times { create(scope, project: project1) }
+ 3.times { create(scope, project: project2) }
+ 3.times { create(scope, project: project3) }
+ end
+
+ shared_examples_for 'deleting inconsistent internal_id records' do
+ before do
+ create_models
+
+ internal_id_query.call(project1).first.tap do |iid|
+ iid.last_value = iid.last_value - 2
+ # This is an inconsistent record
+ iid.save!
+ end
+
+ internal_id_query.call(project3).first.tap do |iid|
+ iid.last_value = iid.last_value + 2
+ # This is a consistent record
+ iid.save!
+ end
+ end
+
+ it "deletes inconsistent issues" do
+ expect { migrate! }.to change { internal_id_query.call(project1).size }.from(1).to(0)
+ end
+
+ it "retains consistent issues" do
+ expect { migrate! }.not_to change { internal_id_query.call(project2).size }
+ end
+
+ it "retains consistent records, especially those with a greater last_value" do
+ expect { migrate! }.not_to change { internal_id_query.call(project3).size }
+ end
+ end
+
+ context 'for issues' do
+ let(:scope) { :issue }
+ it_behaves_like 'deleting inconsistent internal_id records'
+ end
+
+ context 'for merge_requests' do
+ let(:scope) { :merge_request }
+
+ let(:create_models) do
+ 3.times { |i| create(scope, target_project: project1, source_project: project1, source_branch: i.to_s) }
+ 3.times { |i| create(scope, target_project: project2, source_project: project2, source_branch: i.to_s) }
+ 3.times { |i| create(scope, target_project: project3, source_project: project3, source_branch: i.to_s) }
+ end
+
+ it_behaves_like 'deleting inconsistent internal_id records'
+ end
+
+ context 'for deployments' do
+ let(:scope) { :deployment }
+ it_behaves_like 'deleting inconsistent internal_id records'
+ end
+
+ context 'for milestones (by project)' do
+ let(:scope) { :milestone }
+ it_behaves_like 'deleting inconsistent internal_id records'
+ end
+
+ context 'for ci_pipelines' do
+ let(:scope) { :ci_pipeline }
+ it_behaves_like 'deleting inconsistent internal_id records'
+ end
+
+ context 'for milestones (by group)' do
+ # milestones (by group) is a little different than all of the other models
+ let!(:group1) { create(:group) }
+ let!(:group2) { create(:group) }
+ let!(:group3) { create(:group) }
+
+ let(:internal_id_query) { ->(group) { InternalId.where(usage: InternalId.usages['milestones'], namespace: group) } }
+
+ before do
+ 3.times { create(:milestone, group: group1) }
+ 3.times { create(:milestone, group: group2) }
+ 3.times { create(:milestone, group: group3) }
+
+ internal_id_query.call(group1).first.tap do |iid|
+ iid.last_value = iid.last_value - 2
+ # This is an inconsistent record
+ iid.save!
+ end
+
+ internal_id_query.call(group3).first.tap do |iid|
+ iid.last_value = iid.last_value + 2
+ # This is a consistent record
+ iid.save!
+ end
+ end
+
+ it "deletes inconsistent issues" do
+ expect { migrate! }.to change { internal_id_query.call(group1).size }.from(1).to(0)
+ end
+
+ it "retains consistent issues" do
+ expect { migrate! }.not_to change { internal_id_query.call(group2).size }
+ end
+
+ it "retains consistent records, especially those with a greater last_value" do
+ expect { migrate! }.not_to change { internal_id_query.call(group3).size }
+ end
+ end
+end
diff --git a/spec/migrations/migrate_null_wiki_access_levels_spec.rb b/spec/migrations/migrate_null_wiki_access_levels_spec.rb
new file mode 100644
index 00000000000..f99273072a2
--- /dev/null
+++ b/spec/migrations/migrate_null_wiki_access_levels_spec.rb
@@ -0,0 +1,29 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20180809195358_migrate_null_wiki_access_levels.rb')
+
+describe MigrateNullWikiAccessLevels, :migration do
+ let(:namespaces) { table('namespaces') }
+ let(:projects) { table(:projects) }
+ let(:project_features) { table(:project_features) }
+ let(:migration) { described_class.new }
+
+ before do
+ namespace = namespaces.create(name: 'foo', path: 'foo')
+
+ projects.create!(id: 1, name: 'gitlab1', path: 'gitlab1', namespace_id: namespace.id)
+ projects.create!(id: 2, name: 'gitlab2', path: 'gitlab2', namespace_id: namespace.id)
+ projects.create!(id: 3, name: 'gitlab3', path: 'gitlab3', namespace_id: namespace.id)
+
+ project_features.create!(id: 1, project_id: 1, wiki_access_level: nil)
+ project_features.create!(id: 2, project_id: 2, wiki_access_level: 10)
+ project_features.create!(id: 3, project_id: 3, wiki_access_level: 20)
+ end
+
+ describe '#up' do
+ it 'migrates existing project_features with wiki_access_level NULL to 20' do
+ expect { migration.up }.to change { project_features.where(wiki_access_level: 20).count }.by(1)
+ end
+ end
+end
diff --git a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
index 96bef107599..c4427910518 100644
--- a/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
+++ b/spec/migrations/schedule_create_gpg_key_subkeys_from_gpg_keys_spec.rb
@@ -6,7 +6,7 @@ describe ScheduleCreateGpgKeySubkeysFromGpgKeys, :migration, :sidekiq do
create(:gpg_key, id: 1, key: GpgHelpers::User1.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs
create(:gpg_key, id: 2, key: GpgHelpers::User3.public_key) # rubocop:disable RSpec/FactoriesInMigrationSpecs
# Delete all subkeys so they can be recreated
- GpgKeySubkey.destroy_all
+ GpgKeySubkey.destroy_all # rubocop: disable DestroyAll
end
it 'correctly schedules background migrations' do
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
index 25bf596fddc..60d04562e6c 100644
--- a/spec/models/fork_network_member_spec.rb
+++ b/spec/models/fork_network_member_spec.rb
@@ -11,7 +11,7 @@ describe ForkNetworkMember do
let(:fork_network) { fork_network_member.fork_network }
it 'removes the fork network if it was the last member' do
- fork_network.fork_network_members.destroy_all
+ fork_network.fork_network_members.destroy_all # rubocop: disable DestroyAll
expect(ForkNetwork.count).to eq(0)
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 01129df1107..edd1cb455af 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -73,7 +73,7 @@ describe SystemHook do
it "project_destroy hook" do
project.add_maintainer(user)
- project.project_members.destroy_all
+ project.project_members.destroy_all # rubocop: disable DestroyAll
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/,
@@ -110,7 +110,7 @@ describe SystemHook do
it 'group member destroy hook' do
group.add_maintainer(user)
- group.group_members.destroy_all
+ group.group_members.destroy_all # rubocop: disable DestroyAll
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/,
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6258bfa232f..48f4e53b93e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1764,7 +1764,7 @@ describe MergeRequest do
context 'with no discussions' do
before do
- merge_request.notes.destroy_all
+ merge_request.notes.destroy_all # rubocop: disable DestroyAll
end
it 'returns true' do
diff --git a/spec/models/project_auto_devops_spec.rb b/spec/models/project_auto_devops_spec.rb
index 749b2094787..797d767465a 100644
--- a/spec/models/project_auto_devops_spec.rb
+++ b/spec/models/project_auto_devops_spec.rb
@@ -100,7 +100,7 @@ describe ProjectAutoDevops do
end
end
- describe '#set_gitlab_deploy_token' do
+ describe '#create_gitlab_deploy_token' do
let(:auto_devops) { build(:project_auto_devops, project: project) }
context 'when the project is public' do
@@ -144,9 +144,9 @@ describe ProjectAutoDevops do
end
end
- context 'when autodevops is enabled at instancel level' do
+ context 'when autodevops is enabled at instance level' do
let(:project) { create(:project, :repository, :internal) }
- let(:auto_devops) { build(:project_auto_devops, :disabled, project: project) }
+ let(:auto_devops) { build(:project_auto_devops, enabled: nil, project: project) }
it 'should create a deploy token' do
allow(Gitlab::CurrentSettings).to receive(:auto_devops_enabled?).and_return(true)
diff --git a/spec/models/project_group_link_spec.rb b/spec/models/project_group_link_spec.rb
index 1fccf92627a..5bea21427d4 100644
--- a/spec/models/project_group_link_spec.rb
+++ b/spec/models/project_group_link_spec.rb
@@ -41,7 +41,7 @@ describe ProjectGroupLink do
project.project_group_links.create(group: group)
group_users.each { |user| expect(user.authorized_projects).to include(project) }
- project.project_group_links.destroy_all
+ project.project_group_links.destroy_all # rubocop: disable DestroyAll
group_users.each { |user| expect(user.authorized_projects).not_to include(project) }
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index 076de06cf99..d8a5e5f6869 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -3259,6 +3259,11 @@ describe Project do
end
describe '#auto_devops_enabled?' do
+ before do
+ allow(Feature).to receive(:enabled?).and_call_original
+ Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
+ end
+
set(:project) { create(:project) }
subject { project.auto_devops_enabled? }
@@ -3268,19 +3273,14 @@ describe Project do
stub_application_setting(auto_devops_enabled: true)
end
- it 'auto devops is implicitly enabled' do
- expect(project.auto_devops).to be_nil
- expect(project).to be_auto_devops_enabled
- end
+ it { is_expected.to be_truthy }
context 'when explicitly enabled' do
before do
create(:project_auto_devops, project: project)
end
- it "auto devops is enabled" do
- expect(project).to be_auto_devops_enabled
- end
+ it { is_expected.to be_truthy }
end
context 'when explicitly disabled' do
@@ -3288,9 +3288,7 @@ describe Project do
create(:project_auto_devops, project: project, enabled: false)
end
- it "auto devops is disabled" do
- expect(project).not_to be_auto_devops_enabled
- end
+ it { is_expected.to be_falsey }
end
end
@@ -3299,19 +3297,22 @@ describe Project do
stub_application_setting(auto_devops_enabled: false)
end
- it 'auto devops is implicitly disabled' do
- expect(project.auto_devops).to be_nil
- expect(project).not_to be_auto_devops_enabled
- end
+ it { is_expected.to be_falsey }
context 'when explicitly enabled' do
before do
create(:project_auto_devops, project: project)
end
- it "auto devops is enabled" do
- expect(project).to be_auto_devops_enabled
+ it { is_expected.to be_truthy }
+ end
+
+ context 'when force_autodevops_on_by_default is enabled for the project' do
+ before do
+ Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
end
+
+ it { is_expected.to be_truthy }
end
end
end
@@ -3361,6 +3362,11 @@ describe Project do
end
describe '#has_auto_devops_implicitly_disabled?' do
+ before do
+ allow(Feature).to receive(:enabled?).and_call_original
+ Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(0)
+ end
+
set(:project) { create(:project) }
context 'when enabled in settings' do
@@ -3382,6 +3388,16 @@ describe Project do
expect(project).to have_auto_devops_implicitly_disabled
end
+ context 'when force_autodevops_on_by_default is enabled for the project' do
+ before do
+ Feature.get(:force_autodevops_on_by_default).enable_percentage_of_actors(100)
+ end
+
+ it 'does not have auto devops implicitly disabled' do
+ expect(project).not_to have_auto_devops_implicitly_disabled
+ end
+ end
+
context 'when explicitly disabled' do
before do
create(:project_auto_devops, project: project, enabled: false)
diff --git a/spec/policies/group_policy_spec.rb b/spec/policies/group_policy_spec.rb
index 35951251bc5..615fea11f26 100644
--- a/spec/policies/group_policy_spec.rb
+++ b/spec/policies/group_policy_spec.rb
@@ -205,7 +205,7 @@ describe GroupPolicy do
nested_group.add_guest(developer)
nested_group.add_guest(maintainer)
- group.owners.destroy_all
+ group.owners.destroy_all # rubocop: disable DestroyAll
group.add_guest(owner)
nested_group.add_owner(owner)
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index bc45a63d9f1..d3f81cc038d 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -83,11 +83,6 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(403)
end
end
-
- it "returns a 404 error if hook id is not available" do
- get api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
end
describe "POST /projects/:id/hooks" do
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index e3fb6cecce9..bc06f3c3732 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -42,7 +42,7 @@ describe API::ProjectImport do
expect(response).to have_gitlab_http_status(201)
end
- it 'does not shedule an import for a nampespace that does not exist' do
+ it 'does not schedule an import for a namespace that does not exist' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
expect(::Projects::CreateService).not_to receive(:new)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index eb41750bf47..c249c881db5 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -20,7 +20,6 @@ describe API::Projects do
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:project2) { create(:project, namespace: user.namespace) }
- let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
@@ -575,7 +574,7 @@ describe API::Projects do
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
post api('/projects', user), project
@@ -583,7 +582,7 @@ describe API::Projects do
expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do
+ it 'sets a project as allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: true)
post api('/projects', user), project
@@ -698,7 +697,7 @@ describe API::Projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
- it 'returns projects filetered by minimal access level' do
+ it 'returns projects filtered by minimal access level' do
private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace)
private_project2 = create(:project, :private, name: 'private_project2', creator_id: user4.id, namespace: user4.namespace)
private_project1.add_developer(user2)
@@ -789,7 +788,7 @@ describe API::Projects do
expect(json_response['visibility']).to eq('private')
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
post api("/projects/user/#{user.id}", admin), project
@@ -1119,100 +1118,6 @@ describe API::Projects do
end
end
- describe 'GET /projects/:id/snippets' do
- before do
- snippet
- end
-
- it 'returns an array of project snippets' do
- get api("/projects/#{project.id}/snippets", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['title']).to eq(snippet.title)
- end
- end
-
- describe 'GET /projects/:id/snippets/:snippet_id' do
- it 'returns a project snippet' do
- get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(snippet.title)
- end
-
- it 'returns a 404 error if snippet id not found' do
- get api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/snippets' do
- it 'creates a new project snippet' do
- post api("/projects/#{project.id}/snippets", user),
- title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private'
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('api test')
- end
-
- it 'returns a 400 error if invalid snippet is given' do
- post api("/projects/#{project.id}/snippets", user)
- expect(status).to eq(400)
- end
- end
-
- describe 'PUT /projects/:id/snippets/:snippet_id' do
- it 'updates an existing project snippet' do
- put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
- code: 'updated code'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('example')
- expect(snippet.reload.content).to eq('updated code')
- end
-
- it 'updates an existing project snippet with new title' do
- put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
- title: 'other api test'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('other api test')
- end
- end
-
- describe 'DELETE /projects/:id/snippets/:snippet_id' do
- before do
- snippet
- end
-
- it 'deletes existing project snippet' do
- expect do
- delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
-
- expect(response).to have_gitlab_http_status(204)
- end.to change { Snippet.count }.by(-1)
- end
-
- it 'returns 404 when deleting unknown snippet id' do
- delete api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}", user) }
- end
- end
-
- describe 'GET /projects/:id/snippets/:snippet_id/raw' do
- it 'gets a raw project snippet' do
- get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'returns a 404 error if raw project snippet not found' do
- get api("/projects/#{project.id}/snippets/5555/raw", user)
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
describe 'fork management' do
let(:project_fork_target) { create(:project) }
let(:project_fork_source) { create(:project, :public) }
@@ -1235,7 +1140,7 @@ describe API::Projects do
expect(project_fork_target.forked?).to be_truthy
end
- it 'refreshes the forks count cachce' do
+ it 'refreshes the forks count cache' do
expect(project_fork_source.forks_count).to be_zero
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb
new file mode 100644
index 00000000000..b0bc40552b3
--- /dev/null
+++ b/spec/rubocop/cop/destroy_all_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/destroy_all'
+
+describe RuboCop::Cop::DestroyAll do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags the use of destroy_all with a send receiver' do
+ inspect_source('foo.destroy_all # rubocop: disable DestroyAll')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all with a constant receiver' do
+ inspect_source('User.destroy_all # rubocop: disable DestroyAll')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all when passing arguments' do
+ inspect_source('User.destroy_all([])')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all with a local variable receiver' do
+ inspect_source(<<~RUBY)
+ users = User.all
+ users.destroy_all # rubocop: disable DestroyAll
+ RUBY
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'does not flag the use of delete_all' do
+ inspect_source('foo.delete_all')
+
+ expect(cop.offenses).to be_empty
+ end
+end
diff --git a/spec/services/groups/destroy_service_spec.rb b/spec/services/groups/destroy_service_spec.rb
index b54491cf5f9..d80d0f5a8a8 100644
--- a/spec/services/groups/destroy_service_spec.rb
+++ b/spec/services/groups/destroy_service_spec.rb
@@ -135,6 +135,17 @@ describe Groups::DestroyService do
it_behaves_like 'group destruction', false
end
+ context 'repository removal status is taken into account' do
+ it 'raises exception' do
+ expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).and_return(false)
+ end
+
+ expect { destroy_group(group, user, false) }
+ .to raise_error(Groups::DestroyService::DestroyError, "Project #{project.id} can't be deleted" )
+ end
+ end
+
describe 'repository removal' do
before do
destroy_group(group, user, false)
diff --git a/spec/services/merge_requests/create_service_spec.rb b/spec/services/merge_requests/create_service_spec.rb
index 06fb61baf33..74bcc15f912 100644
--- a/spec/services/merge_requests/create_service_spec.rb
+++ b/spec/services/merge_requests/create_service_spec.rb
@@ -134,9 +134,11 @@ describe MergeRequests::CreateService do
let!(:pipeline_3) { create(:ci_pipeline, project: project, ref: "other_branch", project_id: project.id) }
before do
+ # rubocop: disable DestroyAll
project.merge_requests
.where(source_branch: opts[:source_branch], target_branch: opts[:target_branch])
.destroy_all
+ # rubocop: enable DestroyAll
end
it 'sets head pipeline' do
diff --git a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
index 1c632847940..6268c149fc6 100644
--- a/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
+++ b/spec/services/merge_requests/delete_non_latest_diffs_service_spec.rb
@@ -46,10 +46,12 @@ describe MergeRequests::DeleteNonLatestDiffsService, :clean_gitlab_redis_shared_
end
it 'schedules no removal if there is no non-latest diffs' do
+ # rubocop: disable DestroyAll
merge_request
.merge_request_diffs
.where.not(id: merge_request.latest_merge_request_diff_id)
.destroy_all
+ # rubocop: enable DestroyAll
expect(DeleteDiffFilesWorker).not_to receive(:bulk_perform_in)
diff --git a/spec/services/projects/detect_repository_languages_service_spec.rb b/spec/services/projects/detect_repository_languages_service_spec.rb
index f90d558938f..deea1189cdf 100644
--- a/spec/services/projects/detect_repository_languages_service_spec.rb
+++ b/spec/services/projects/detect_repository_languages_service_spec.rb
@@ -5,10 +5,6 @@ describe Projects::DetectRepositoryLanguagesService, :clean_gitlab_redis_shared_
subject { described_class.new(project, project.owner) }
- before do
- allow(Feature).to receive(:disabled?).and_return(false)
- end
-
describe '#execute' do
context 'without previous detection' do
it 'inserts new programming languages in the database' do
diff --git a/spec/services/todo_service_spec.rb b/spec/services/todo_service_spec.rb
index 9a51c873b30..1746721b0d0 100644
--- a/spec/services/todo_service_spec.rb
+++ b/spec/services/todo_service_spec.rb
@@ -280,7 +280,7 @@ describe TodoService do
end
it 'does not create a todo if unassigned' do
- issue.assignees.destroy_all
+ issue.assignees.destroy_all # rubocop: disable DestroyAll
should_not_create_any_todo { service.reassigned_issue(issue, author) }
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index 3bae8bfbd42..83f1495a1c6 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -20,7 +20,7 @@ describe Users::DestroyService do
it 'will delete the project' do
expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
service.execute(user)
@@ -35,7 +35,7 @@ describe Users::DestroyService do
it 'destroys a project in pending_delete' do
expect_next_instance_of(Projects::DestroyService) do |destroy_service|
- expect(destroy_service).to receive(:execute).once
+ expect(destroy_service).to receive(:execute).once.and_return(true)
end
service.execute(user)
@@ -172,23 +172,36 @@ describe Users::DestroyService do
end
describe "user personal's repository removal" do
- before do
- perform_enqueued_jobs { service.execute(user) }
- end
+ context 'storages' do
+ before do
+ perform_enqueued_jobs { service.execute(user) }
+ end
+
+ context 'legacy storage' do
+ let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
+
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ end
+ end
- context 'legacy storage' do
- let!(:project) { create(:project, :empty_repo, :legacy_storage, namespace: user.namespace) }
+ context 'hashed storage' do
+ let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
- it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ it 'removes repository' do
+ expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ end
end
end
- context 'hashed storage' do
- let!(:project) { create(:project, :empty_repo, namespace: user.namespace) }
+ context 'repository removal status is taken into account' do
+ it 'raises exception' do
+ expect_next_instance_of(::Projects::DestroyService) do |destroy_service|
+ expect(destroy_service).to receive(:execute).and_return(false)
+ end
- it 'removes repository' do
- expect(gitlab_shell.exists?(project.repository_storage, "#{project.disk_path}.git")).to be_falsey
+ expect { service.execute(user) }
+ .to raise_error(Users::DestroyService::DestroyError, "Project #{project.id} can't be deleted" )
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index bd564cc60a6..a15a46a9534 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -29,6 +29,7 @@ end
# require rainbow gem String monkeypatch, so we can test SystemChecks
require 'rainbow/ext/string'
+Rainbow.enabled = false
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
@@ -111,6 +112,13 @@ RSpec.configure do |config|
config.before(:example) do
# Enable all features by default for testing
allow(Feature).to receive(:enabled?) { true }
+
+ # The following can be removed when we remove the staged rollout strategy
+ # and we can just enable it using instance wide settings
+ # (ie. ApplicationSetting#auto_devops_enabled)
+ allow(Feature).to receive(:enabled?)
+ .with(:force_autodevops_on_by_default, anything)
+ .and_return(false)
end
config.before(:example, :request_store) do
diff --git a/spec/support/api/milestones_shared_examples.rb b/spec/support/api/milestones_shared_examples.rb
index a15189db35f..afd6448aa26 100644
--- a/spec/support/api/milestones_shared_examples.rb
+++ b/spec/support/api/milestones_shared_examples.rb
@@ -102,14 +102,6 @@ shared_examples_for 'group and project milestones' do |route_definition|
expect(json_response['iid']).to eq(milestone.iid)
end
- it 'returns a milestone by id' do
- get api(resource_route, user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(milestone.title)
- expect(json_response['iid']).to eq(milestone.iid)
- end
-
it 'returns 401 error if user not authenticated' do
get api(resource_route)
diff --git a/spec/support/shared_examples/fast_destroy_all.rb b/spec/support/shared_examples/fast_destroy_all.rb
index 5448ddcfe33..a8079b6d864 100644
--- a/spec/support/shared_examples/fast_destroy_all.rb
+++ b/spec/support/shared_examples/fast_destroy_all.rb
@@ -4,8 +4,8 @@ shared_examples_for 'fast destroyable' do
expect(external_data_counter).to be > 0
expect(subjects.count).to be > 0
- expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
- expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbbiden. Please use `fast_destroy_all`')
+ expect { subjects.first.destroy }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`')
+ expect { subjects.destroy_all }.to raise_error('`destroy` and `destroy_all` are forbidden. Please use `fast_destroy_all`') # rubocop: disable DestroyAll
expect(subjects.count).to be > 0
expect(external_data_counter).to be > 0
diff --git a/spec/tasks/gitlab/site_statistics_rake_spec.rb b/spec/tasks/gitlab/site_statistics_rake_spec.rb
new file mode 100644
index 00000000000..20f0df65e63
--- /dev/null
+++ b/spec/tasks/gitlab/site_statistics_rake_spec.rb
@@ -0,0 +1,24 @@
+# frozen_string_literal: true
+require 'rake_helper'
+
+describe 'rake gitlab:refresh_site_statistics' do
+ before do
+ Rake.application.rake_require 'tasks/gitlab/site_statistics'
+
+ create(:project)
+ SiteStatistic.fetch.update(repositories_count: 0, wikis_count: 0)
+ end
+
+ let(:task) { 'gitlab:refresh_site_statistics' }
+
+ it 'recalculates existing counters' do
+ run_rake_task(task)
+
+ expect(SiteStatistic.fetch.repositories_count).to eq(1)
+ expect(SiteStatistic.fetch.wikis_count).to eq(1)
+ end
+
+ it 'displays message listing counters' do
+ expect { run_rake_task(task) }.to output(/Updating Site Statistics counters:.* Repositories\.\.\. OK!.* Wikis\.\.\. OK!/m).to_stdout
+ end
+end
diff --git a/spec/tasks/gitlab/traces_rake_spec.rb b/spec/tasks/gitlab/traces_rake_spec.rb
index bd18e8ffc1e..aaf0d7242dd 100644
--- a/spec/tasks/gitlab/traces_rake_spec.rb
+++ b/spec/tasks/gitlab/traces_rake_spec.rb
@@ -5,51 +5,109 @@ describe 'gitlab:traces rake tasks' do
Rake.application.rake_require 'tasks/gitlab/traces'
end
- shared_examples 'passes the job id to worker' do
- it do
- expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]])
+ describe 'gitlab:traces:archive' do
+ shared_examples 'passes the job id to worker' do
+ it do
+ expect(ArchiveTraceWorker).to receive(:bulk_perform_async).with([[job.id]])
- run_rake_task('gitlab:traces:archive')
+ run_rake_task('gitlab:traces:archive')
+ end
end
- end
- shared_examples 'does not pass the job id to worker' do
- it do
- expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async)
+ shared_examples 'does not pass the job id to worker' do
+ it do
+ expect(ArchiveTraceWorker).not_to receive(:bulk_perform_async)
- run_rake_task('gitlab:traces:archive')
+ run_rake_task('gitlab:traces:archive')
+ end
end
- end
- context 'when trace file stored in default path' do
- let!(:job) { create(:ci_build, :success, :trace_live) }
+ context 'when trace file stored in default path' do
+ let!(:job) { create(:ci_build, :success, :trace_live) }
- it_behaves_like 'passes the job id to worker'
- end
+ it_behaves_like 'passes the job id to worker'
+ end
- context 'when trace is stored in database' do
- let!(:job) { create(:ci_build, :success) }
+ context 'when trace is stored in database' do
+ let!(:job) { create(:ci_build, :success) }
- before do
- job.update_column(:trace, 'trace in db')
+ before do
+ job.update_column(:trace, 'trace in db')
+ end
+
+ it_behaves_like 'passes the job id to worker'
end
- it_behaves_like 'passes the job id to worker'
+ context 'when job has trace artifact' do
+ let!(:job) { create(:ci_build, :success) }
+
+ before do
+ create(:ci_job_artifact, :trace, job: job)
+ end
+
+ it_behaves_like 'does not pass the job id to worker'
+ end
+
+ context 'when job is not finished yet' do
+ let!(:build) { create(:ci_build, :running, :trace_live) }
+
+ it_behaves_like 'does not pass the job id to worker'
+ end
end
- context 'when job has trace artifact' do
- let!(:job) { create(:ci_build, :success) }
+ describe 'gitlab:traces:migrate' do
+ let(:object_storage_enabled) { false }
before do
- create(:ci_job_artifact, :trace, job: job)
+ stub_artifacts_object_storage(enabled: object_storage_enabled)
end
- it_behaves_like 'does not pass the job id to worker'
- end
+ subject { run_rake_task('gitlab:traces:migrate') }
- context 'when job is not finished yet' do
- let!(:build) { create(:ci_build, :running, :trace_live) }
+ let!(:job_trace) { create(:ci_job_artifact, :trace, file_store: store) }
- it_behaves_like 'does not pass the job id to worker'
+ context 'when local storage is used' do
+ let(:store) { ObjectStorage::Store::LOCAL }
+
+ context 'and job does not have file store defined' do
+ let(:object_storage_enabled) { true }
+ let(:store) { nil }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is defined' do
+ let(:object_storage_enabled) { true }
+
+ it "migrates file to remote storage" do
+ subject
+
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
+
+ context 'and remote storage is not defined' do
+ it "fails to migrate to remote storage" do
+ subject
+
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::LOCAL)
+ end
+ end
+ end
+
+ context 'when remote storage is used' do
+ let(:object_storage_enabled) { true }
+ let(:store) { ObjectStorage::Store::REMOTE }
+
+ it "file stays on remote storage" do
+ subject
+
+ expect(job_trace.reload.file_store).to eq(ObjectStorage::Store::REMOTE)
+ end
+ end
end
end
diff --git a/spec/workers/project_destroy_worker_spec.rb b/spec/workers/project_destroy_worker_spec.rb
index 42e1d86e3bb..6132f145f8d 100644
--- a/spec/workers/project_destroy_worker_spec.rb
+++ b/spec/workers/project_destroy_worker_spec.rb
@@ -18,13 +18,6 @@ describe ProjectDestroyWorker do
expect(Dir.exist?(path)).to be_falsey
end
- it 'deletes the project but skips repo deletion' do
- subject.perform(project.id, project.owner.id, { "skip_repo" => true })
-
- expect(Project.all).not_to include(project)
- expect(Dir.exist?(path)).to be_truthy
- end
-
it 'does not raise error when project could not be found' do
expect do
subject.perform(-1, project.owner.id, {})
diff --git a/spec/workers/repository_check/single_repository_worker_spec.rb b/spec/workers/repository_check/single_repository_worker_spec.rb
index 22fc64c1536..f11875cffd1 100644
--- a/spec/workers/repository_check/single_repository_worker_spec.rb
+++ b/spec/workers/repository_check/single_repository_worker_spec.rb
@@ -6,7 +6,7 @@ describe RepositoryCheck::SingleRepositoryWorker do
it 'skips when the project has no push events' do
project = create(:project, :repository, :wiki_disabled)
- project.events.destroy_all
+ project.events.destroy_all # rubocop: disable DestroyAll
break_project(project)
expect(worker).not_to receive(:git_fsck)
diff --git a/yarn.lock b/yarn.lock
index c1e9d0ab73e..4326245d2ac 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -94,10 +94,34 @@
version "0.7.0"
resolved "https://registry.yarnpkg.com/@sindresorhus/is/-/is-0.7.0.tgz#9a06f4f137ee84d7df0460c1fdb1135ffa6c50fd"
+"@types/events@*":
+ version "1.2.0"
+ resolved "https://registry.yarnpkg.com/@types/events/-/events-1.2.0.tgz#81a6731ce4df43619e5c8c945383b3e62a89ea86"
+
+"@types/glob@^5":
+ version "5.0.35"
+ resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.35.tgz#1ae151c802cece940443b5ac246925c85189f32a"
+ dependencies:
+ "@types/events" "*"
+ "@types/minimatch" "*"
+ "@types/node" "*"
+
"@types/jquery@^2.0.40":
version "2.0.48"
resolved "https://registry.yarnpkg.com/@types/jquery/-/jquery-2.0.48.tgz#3e90d8cde2d29015e5583017f7830cb3975b2eef"
+"@types/minimatch@*":
+ version "3.0.3"
+ resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.3.tgz#3dca0e3f33b200fc7d1139c0cd96c1268cadfd9d"
+
+"@types/node@*":
+ version "10.5.2"
+ resolved "https://registry.yarnpkg.com/@types/node/-/node-10.5.2.tgz#f19f05314d5421fe37e74153254201a7bf00a707"
+
+"@types/parse5@^5":
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/@types/parse5/-/parse5-5.0.0.tgz#9ae2106efc443d7c1e26570aa8247828c9c80f11"
+
"@vue/component-compiler-utils@^1.2.1":
version "1.2.1"
resolved "https://registry.yarnpkg.com/@vue/component-compiler-utils/-/component-compiler-utils-1.2.1.tgz#3d543baa75cfe5dab96e29415b78366450156ef6"
@@ -2099,6 +2123,10 @@ css-loader@^1.0.0:
postcss-value-parser "^3.3.0"
source-list-map "^2.0.0"
+css-selector-parser@^1.3:
+ version "1.3.0"
+ resolved "https://registry.yarnpkg.com/css-selector-parser/-/css-selector-parser-1.3.0.tgz#5f1ad43e2d8eefbfdc304fcd39a521664943e3eb"
+
css-selector-tokenizer@^0.7.0:
version "0.7.0"
resolved "https://registry.yarnpkg.com/css-selector-tokenizer/-/css-selector-tokenizer-0.7.0.tgz#e6988474ae8c953477bf5e7efecfceccd9cf4c86"
@@ -3520,6 +3548,26 @@ getpass@^0.1.1:
dependencies:
assert-plus "^1.0.0"
+gettext-extractor-vue@^4.0.1:
+ version "4.0.1"
+ resolved "https://registry.yarnpkg.com/gettext-extractor-vue/-/gettext-extractor-vue-4.0.1.tgz#69d2737eb8f1938803ffcf9317133ed59fb2372f"
+ dependencies:
+ bluebird "^3.5.1"
+ glob "^7.1.2"
+ vue-template-compiler "^2.5.0"
+
+gettext-extractor@^3.3.2:
+ version "3.3.2"
+ resolved "https://registry.yarnpkg.com/gettext-extractor/-/gettext-extractor-3.3.2.tgz#d5172ba8d175678bd40a5abe7f908fa2a9d9473b"
+ dependencies:
+ "@types/glob" "^5"
+ "@types/parse5" "^5"
+ css-selector-parser "^1.3"
+ glob "5 - 7"
+ parse5 "^5"
+ pofile "^1"
+ typescript "^2"
+
glob-parent@^3.1.0:
version "3.1.0"
resolved "https://registry.yarnpkg.com/glob-parent/-/glob-parent-3.1.0.tgz#9e6af6299d8d3bd2bd40430832bd113df906c5ae"
@@ -3527,24 +3575,24 @@ glob-parent@^3.1.0:
is-glob "^3.1.0"
path-dirname "^1.0.0"
-glob@^5.0.15:
- version "5.0.15"
- resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
+"glob@5 - 7", glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
+ version "7.1.2"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
dependencies:
+ fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "2 || 3"
+ minimatch "^3.0.4"
once "^1.3.0"
path-is-absolute "^1.0.0"
-glob@^7.0.3, glob@^7.0.5, glob@^7.1.1, glob@^7.1.2:
- version "7.1.2"
- resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15"
+glob@^5.0.15:
+ version "5.0.15"
+ resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
dependencies:
- fs.realpath "^1.0.0"
inflight "^1.0.4"
inherits "2"
- minimatch "^3.0.4"
+ minimatch "2 || 3"
once "^1.3.0"
path-is-absolute "^1.0.0"
@@ -5750,6 +5798,10 @@ parse-json@^2.2.0:
dependencies:
error-ex "^1.2.0"
+parse5@^5:
+ version "5.0.0"
+ resolved "https://registry.yarnpkg.com/parse5/-/parse5-5.0.0.tgz#4d02710d44f3c3846197a11e205d4ef17842b81a"
+
parseqs@0.0.5:
version "0.0.5"
resolved "https://registry.yarnpkg.com/parseqs/-/parseqs-0.0.5.tgz#d5208a3738e46766e291ba2ea173684921a8b89d"
@@ -5888,6 +5940,10 @@ pluralize@^7.0.0:
version "7.0.0"
resolved "https://registry.yarnpkg.com/pluralize/-/pluralize-7.0.0.tgz#298b89df8b93b0221dbf421ad2b1b1ea23fc6777"
+pofile@^1:
+ version "1.0.11"
+ resolved "https://registry.yarnpkg.com/pofile/-/pofile-1.0.11.tgz#35aff58c17491d127a07336d5522ebc9df57c954"
+
popper.js@^1.12.9, popper.js@^1.14.3:
version "1.14.3"
resolved "https://registry.yarnpkg.com/popper.js/-/popper.js-1.14.3.tgz#1438f98d046acf7b4d78cd502bf418ac64d4f095"
@@ -7460,6 +7516,10 @@ typedarray@^0.0.6:
version "0.0.6"
resolved "https://registry.yarnpkg.com/typedarray/-/typedarray-0.0.6.tgz#867ac74e3864187b1d3d47d996a78ec5c8830777"
+typescript@^2:
+ version "2.9.2"
+ resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.9.2.tgz#1cbf61d05d6b96269244eb6a3bce4bd914e0f00c"
+
uglify-es@^3.3.4:
version "3.3.9"
resolved "https://registry.yarnpkg.com/uglify-es/-/uglify-es-3.3.9.tgz#0c1c4f0700bed8dbc124cdb304d2592ca203e677"
@@ -7756,7 +7816,7 @@ vue-style-loader@^4.1.0:
hash-sum "^1.0.2"
loader-utils "^1.0.2"
-vue-template-compiler@^2.5.16:
+vue-template-compiler@^2.5.0, vue-template-compiler@^2.5.16:
version "2.5.16"
resolved "https://registry.yarnpkg.com/vue-template-compiler/-/vue-template-compiler-2.5.16.tgz#93b48570e56c720cdf3f051cc15287c26fbd04cb"
dependencies: