summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab/issue_templates/Acceptance_Testing.md100
-rw-r--r--.gitlab/issue_templates/Documentation.md54
-rw-r--r--.gitlab/issue_templates/Security developer workflow.md8
-rw-r--r--.gitlab/merge_request_templates/Change documentation location.md32
-rw-r--r--.gitlab/merge_request_templates/Documentation.md35
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--GITLAB_WORKHORSE_VERSION2
-rw-r--r--Gemfile2
-rw-r--r--Gemfile.lock7
-rw-r--r--Gemfile.rails5.lock5
-rw-r--r--PROCESS.md33
-rw-r--r--app/assets/javascripts/boards/components/board_list.vue2
-rw-r--r--app/assets/javascripts/commons/polyfills.js1
-rw-r--r--app/assets/javascripts/diffs/components/app.vue12
-rw-r--r--app/assets/javascripts/diffs/components/diff_file.vue16
-rw-r--r--app/assets/javascripts/diffs/constants.js3
-rw-r--r--app/assets/javascripts/diffs/store/actions.js21
-rw-r--r--app/assets/javascripts/diffs/store/mutation_types.js1
-rw-r--r--app/assets/javascripts/diffs/store/mutations.js43
-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/ide/stores/mutations.js7
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/jobs/components/artifacts_block.vue98
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue64
-rw-r--r--app/assets/javascripts/jobs/components/empty_state.vue76
-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/admin/application_settings/index.js2
-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/project_select.js8
-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/bootstrap_migration.scss2
-rw-r--r--app/assets/stylesheets/framework/buttons.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss6
-rw-r--r--app/assets/stylesheets/framework/files.scss6
-rw-r--r--app/assets/stylesheets/framework/flash.scss4
-rw-r--r--app/assets/stylesheets/framework/forms.scss2
-rw-r--r--app/assets/stylesheets/framework/markdown_area.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss10
-rw-r--r--app/assets/stylesheets/framework/variables.scss15
-rw-r--r--app/assets/stylesheets/framework/zen.scss2
-rw-r--r--app/assets/stylesheets/page_bundles/ide.scss3
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/builds.scss2
-rw-r--r--app/assets/stylesheets/pages/commits.scss4
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss4
-rw-r--r--app/assets/stylesheets/pages/environments.scss4
-rw-r--r--app/assets/stylesheets/pages/graph.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss10
-rw-r--r--app/assets/stylesheets/pages/issues.scss4
-rw-r--r--app/assets/stylesheets/pages/labels.scss6
-rw-r--r--app/assets/stylesheets/pages/milestone.scss2
-rw-r--r--app/assets/stylesheets/pages/note_form.scss8
-rw-r--r--app/assets/stylesheets/pages/notes.scss31
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss2
-rw-r--r--app/assets/stylesheets/pages/projects.scss4
-rw-r--r--app/assets/stylesheets/pages/search.scss2
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/finders/license_template_finder.rb36
-rw-r--r--app/helpers/application_settings_helper.rb1
-rw-r--r--app/helpers/blob_helper.rb12
-rw-r--r--app/helpers/button_helper.rb6
-rw-r--r--app/helpers/icons_helper.rb2
-rw-r--r--app/helpers/projects_helper.rb5
-rw-r--r--app/mailers/abuse_report_mailer.rb2
-rw-r--r--app/mailers/base_mailer.rb2
-rw-r--r--app/mailers/devise_mailer.rb9
-rw-r--r--app/mailers/email_rejection_mailer.rb2
-rw-r--r--app/mailers/emails/issues.rb2
-rw-r--r--app/mailers/emails/members.rb2
-rw-r--r--app/mailers/emails/merge_requests.rb2
-rw-r--r--app/mailers/emails/notes.rb2
-rw-r--r--app/mailers/emails/pages_domains.rb2
-rw-r--r--app/mailers/emails/pipelines.rb8
-rw-r--r--app/mailers/emails/profile.rb2
-rw-r--r--app/mailers/emails/projects.rb2
-rw-r--r--app/mailers/notify.rb16
-rw-r--r--app/mailers/previews/devise_mailer_preview.rb2
-rw-r--r--app/mailers/previews/email_rejection_mailer_preview.rb2
-rw-r--r--app/mailers/previews/notify_preview.rb2
-rw-r--r--app/mailers/previews/repository_check_mailer_preview.rb2
-rw-r--r--app/mailers/repository_check_mailer.rb2
-rw-r--r--app/models/application_setting.rb3
-rw-r--r--app/models/ci/build.rb5
-rw-r--r--app/models/ci/job_artifact.rb2
-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/license_template.rb53
-rw-r--r--app/models/namespace.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/_account_and_limit.html.haml6
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/application_settings/show.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/_instructions.html.haml2
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/settings/ci_cd/_form.html.haml4
-rw-r--r--app/views/projects/show.html.haml3
-rw-r--r--app/views/search/results/_blob.html.haml2
-rw-r--r--app/views/shared/runners/_form.html.haml30
-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
-rwxr-xr-xbin/secpick4
-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/49905-fix-checkboxes-runners.yml5
-rw-r--r--changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml5
-rw-r--r--changelogs/unreleased/50101-aritfacts-block.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-empty-state-component.yml5
-rw-r--r--changelogs/unreleased/50101-trigger.yml5
-rw-r--r--changelogs/unreleased/50101-truncated-job-information.yml5
-rw-r--r--changelogs/unreleased/50180-fa-icon-google-audit.yml5
-rw-r--r--changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.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/auto-devops-gitlab-ci-glic-228.yml5
-rw-r--r--changelogs/unreleased/bvl-add-czech.yml5
-rw-r--r--changelogs/unreleased/emoji-cutoff-1px.yml5
-rw-r--r--changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml5
-rw-r--r--changelogs/unreleased/frozen-string-enable-app-mailers.yml5
-rw-r--r--changelogs/unreleased/ide-delete-new-files-state.yml5
-rw-r--r--changelogs/unreleased/ide-job-top-bar-ui-polish.yml5
-rw-r--r--changelogs/unreleased/mk-bump-rainbow-gem.yml5
-rw-r--r--changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml6
-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--changelogs/unreleased/tz-mr-incremental-rendering.yml4
-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/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb19
-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.rb3
-rw-r--r--doc/README.md1
-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/operations/ssh_certificates.md17
-rw-r--r--doc/api/jobs.md28
-rw-r--r--doc/api/settings.md8
-rw-r--r--doc/api/system_hooks.md5
-rw-r--r--doc/ci/docker/README.md1
-rw-r--r--doc/ci/docker/using_kaniko.md60
-rw-r--r--doc/ci/examples/README.md4
-rw-r--r--doc/ci/img/junit_test_report.pngbin0 -> 9572 bytes
-rw-r--r--doc/ci/junit_test_reports.md102
-rw-r--r--doc/ci/yaml/README.md50
-rw-r--r--doc/development/documentation/index.md66
-rw-r--r--doc/development/documentation/structure.md149
-rw-r--r--doc/development/documentation/styleguide.md26
-rw-r--r--doc/development/documentation/workflow.md186
-rw-r--r--doc/development/ee_features.md28
-rw-r--r--doc/development/feature_flags.md35
-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/merge_requests/index.md3
-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/api/entities.rb8
-rw-r--r--lib/api/jobs.rb4
-rw-r--r--lib/api/templates.rb44
-rw-r--r--lib/gitlab/git/repository.rb10
-rw-r--r--lib/gitlab/git/repository_mirroring.rb29
-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/i18n.rb3
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/gitlab/template/finders/base_template_finder.rb2
-rw-r--r--lib/gitlab/template/finders/repo_template_finder.rb10
-rw-r--r--lib/tasks/gitlab/site_statistics.rake23
-rw-r--r--lib/tasks/gitlab/traces.rake17
-rw-r--r--locale/gitlab.pot263
-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/factories/uploads.rb7
-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/features/issues/award_emoji_spec.rb146
-rw-r--r--spec/features/issues/award_spec.rb51
-rw-r--r--spec/features/issues/user_interacts_with_awards_spec.rb347
-rw-r--r--spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb2
-rw-r--r--spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb172
-rw-r--r--spec/finders/license_template_finder_spec.rb49
-rw-r--r--spec/helpers/button_helper_spec.rb12
-rw-r--r--spec/helpers/icons_helper_spec.rb20
-rw-r--r--spec/javascripts/boards/board_list_spec.js9
-rw-r--r--spec/javascripts/diffs/components/diff_file_spec.js10
-rw-r--r--spec/javascripts/diffs/mock_data/diff_file.js1
-rw-r--r--spec/javascripts/diffs/store/mutations_spec.js18
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js20
-rw-r--r--spec/javascripts/ide/stores/mutations_spec.js27
-rw-r--r--spec/javascripts/jobs/artifacts_block_spec.js120
-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/empty_state_spec.js90
-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/cleanup/project_uploads_spec.rb2
-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/gitlab/template/finders/repo_template_finders_spec.rb37
-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/internal_id_spec.rb4
-rw-r--r--spec/models/license_template_spec.rb59
-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/jobs_spec.rb74
-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/requests/api/templates_spec.rb3
-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--vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml6
-rw-r--r--yarn.lock80
310 files changed, 5260 insertions, 1148 deletions
diff --git a/.gitlab/issue_templates/Acceptance_Testing.md b/.gitlab/issue_templates/Acceptance_Testing.md
new file mode 100644
index 00000000000..f1fbb96ce61
--- /dev/null
+++ b/.gitlab/issue_templates/Acceptance_Testing.md
@@ -0,0 +1,100 @@
+## Details
+- **Feature Toggle Name**: `FEATURE_NAME`
+- **Required GitLab Version**: `vX.X`
+
+--------------------------------------------------------------------------------
+
+## 1. Preparation
+
+- [ ] **Controllers and workers**:
+ 1. Please link to dashboards of the workers, and the controllers and actions that can be impacted
+ 2. ...
+ 3. ...
+
+## 2. Development Trial
+
+#### Check Dev Server Versions
+- [ ] GitLab: https://dev.gitlab.org/help
+
+#### Enable on `dev.gitlab.org`:
+- [ ] `/chatops feature set FEATURE_NAME true --dev` in [`#dev-gitlab`](https://gitlab.slack.com/messages/C6WQ87MU3)
+
+Then leave running while monitoring and performing some testing through web, api or SSH.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
+
+## 2. Staging Trial
+
+#### Check Staging Server Versions
+- [ ] GitLab: https://staging.gitlab.com/help
+
+#### Enable on `staging.gitlab.com`
+- [ ] `/chatops run feature set FEATURE_NAME true --staging` in [`#development`](https://gitlab.slack.com/messages/C02PF508L/)
+
+Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
+
+## 4. Production Server Version Check
+
+- [ ] GitLab: https://gitlab.com/help
+
+## 5. Initial Impact Check
+
+- [ ] Enable for a subset of users, when using percentage gates: 1%.
+
+Then leave running while monitoring for at least **15 minutes** while performing some testing through web, api or SSH.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
+
+## 6. Low Impact Check
+
+- [ ] Enable for a bigger subset of users, when using percentage gates: 10%.
+
+Then leave running while monitoring for at least **30 minutes** while performing some testing through web, api or SSH.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
+
+## 7. Mid Impact Trial
+
+- [ ] Enable for a big subset of users, when using percentage gates: 50%.
+
+Then leave running while monitoring for at least **12 hours** while performing some testing through web, api or SSH.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Sentry](https://sentry.gitlap.com/gitlab/gitlabcom/?query=is%3Aunresolved)
+
+## 8. Full Impact Trial
+
+- [ ] Enable for all users: `/chatops run feature set FEATURE_NAME true
+
+Then leave running while monitoring for at least **1 week**.
+
+#### Monitor
+
+- [ ] [Monitor Using Grafana](https://dashboards.gitlab.net)
+- [ ] [Inspect logs in ELK](https://log.gitlab.net/app/kibana)
+- [ ] [Check for errors in GitLab Dev Sentry](https://sentry.gitlap.com/gitlab/devgitlaborg/?query=is%3Aunresolved)
+
+#### Success?
+
+- [ ] Remove the feature gate from the code, and close this issue with that MR.
diff --git a/.gitlab/issue_templates/Documentation.md b/.gitlab/issue_templates/Documentation.md
new file mode 100644
index 00000000000..b33ed5bcaa8
--- /dev/null
+++ b/.gitlab/issue_templates/Documentation.md
@@ -0,0 +1,54 @@
+<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation -->
+
+<!-- Mention "documentation" or "docs" in the issue title -->
+
+<!-- Use this description template for new docs or updates to existing docs. -->
+
+<!-- Check the documentation structure guidelines for guidance: https://docs.gitlab.com/ee/development/documentation/structure.html-->
+
+- [ ] Documents Feature A <!-- feature name -->
+- [ ] Follow-up from: #XXX, !YYY <!-- Mention related issues, MRs, and epics when available -->
+
+## New doc or update?
+
+<!-- Mark either of these boxes: -->
+
+- [ ] New documentation
+- [ ] Update existing documentation
+
+## Checklists
+
+### Product Manager
+
+<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#1-product-manager-s-role-in-the-documentation-process -->
+
+- [ ] Add the correct labels
+- [ ] Add the correct milestone
+- [ ] Indicate the correct document/directory for this feature <!-- (ping the tech writers for help if you're not sure) -->
+- [ ] Fill the doc blurb below
+
+#### Documentation blurb
+
+<!-- Documentation template: https://docs.gitlab.com/ee/development/documentation/structure.html#documentation-template-for-new-docs -->
+
+- Doc **title**
+
+ <!-- write the doc title here -->
+
+- Feature **overview/description**
+
+ <!-- Write the feature overview here -->
+
+- Feature **use cases**
+
+ <!-- Write the use cases here -->
+
+### Developer
+
+<!-- Reference: https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process -->
+
+- [ ] Copy the doc blurb above and paste it into the doc
+- [ ] Write the tutorial - explain how to use the feature
+- [ ] Submit the MR using the appropriate MR description template
+
+/label ~Documentation
diff --git a/.gitlab/issue_templates/Security developer workflow.md b/.gitlab/issue_templates/Security developer workflow.md
index c1f702e9385..64b54b171f7 100644
--- a/.gitlab/issue_templates/Security developer workflow.md
+++ b/.gitlab/issue_templates/Security developer workflow.md
@@ -12,7 +12,7 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Link to the original issue adding it to the [links section](#links)
- [ ] Run `scripts/security-harness` in the CE, EE, and/or Omnibus to prevent pushing to any remote besides `dev.gitlab.org`
- [ ] Create an MR targetting `org` `master`, prefixing your branch with `security-`
-- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
+- [ ] Label your MR with the ~security label, prefix the title with `WIP: [master]`
- [ ] Add a link to the MR to the [links section](#links)
- [ ] Add a link to an EE MR if required
- [ ] Make sure the MR remains in-progress and gets approved after the review cycle, **but never merged**.
@@ -22,13 +22,13 @@ Set the title to: `[Security] Description of the original issue`
- [ ] Once the MR is ready to be merged, create MRs targetting the last 3 releases
- [ ] At this point, it might be easy to squash the commits from the MR into one
- - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [seckpick documentation]
+ - You can use the script `bin/secpick` instead of the following steps, to help you cherry-picking. See the [secpick documentation]
- [ ] Create the branch `security-X-Y` from `X-Y-stable` if it doesn't exist (and make sure it's up to date with stable)
- [ ] Create each MR targetting the security branch `security-X-Y`
- [ ] Add the ~security label and prefix with the version `WIP: [X.Y]` the title of the MR
- [ ] Make sure all MRs have a link in the [links section](#links) and are assigned to a Release Manager.
-[seckpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
+[secpick documentation]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md#secpick-script
#### Documentation and final details
@@ -68,4 +68,4 @@ Set the title to: `[Security] Description of the original issue`
[security process for developers]: https://gitlab.com/gitlab-org/release/docs/blob/master/general/security/developer.md
[RM list]: https://about.gitlab.com/release-managers/
-/label ~security
+/label ~security
diff --git a/.gitlab/merge_request_templates/Change documentation location.md b/.gitlab/merge_request_templates/Change documentation location.md
new file mode 100644
index 00000000000..b4a6d2bd3b4
--- /dev/null
+++ b/.gitlab/merge_request_templates/Change documentation location.md
@@ -0,0 +1,32 @@
+<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/ -->
+
+<!-- Use this description template for changing documentation location. For new docs or updates to existing docs, use the "Documentation" template -->
+
+## What does this MR do?
+
+<!-- Briefly describe what this MR is about -->
+
+## Related issues
+
+<!-- Mention the issue(s) this MR closes or is related to -->
+
+Closes
+
+## Moving docs to a new location?
+
+Read the guidelines:
+https://docs.gitlab.com/ce/development/documentation/index.html#changing-document-location
+
+- [ ] Make sure the old link is not removed and has its contents replaced with
+ a link to the new location.
+- [ ] Make sure internal links pointing to the document in question are not broken.
+- [ ] Search and replace any links referring to old docs in GitLab Rails app,
+ specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
+- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ce/development/writing_documentation.html#redirections-for-pages-with-disqus-comments)
+ to the new document if there are any Disqus comments on the old document thread.
+- [ ] Update the link in `features.yml` (if applicable)
+- [ ] If working on CE and the `ee-compat-check` jobs fails, submit an MR to EE
+ with the changes as well (https://docs.gitlab.com/ce/development/writing_documentation.html#cherry-picking-from-ce-to-ee).
+- [ ] Ping one of the technical writers for review.
+
+/label ~Documentation
diff --git a/.gitlab/merge_request_templates/Documentation.md b/.gitlab/merge_request_templates/Documentation.md
index 531035b3766..ca38c881c66 100644
--- a/.gitlab/merge_request_templates/Documentation.md
+++ b/.gitlab/merge_request_templates/Documentation.md
@@ -1,4 +1,8 @@
-<!--See the general Documentation guidelines https://docs.gitlab.com/ee/development/documentation/index.html -->
+<!--See the general documentation guidelines https://docs.gitlab.com/ee/development/documentation -->
+
+<!-- Mention "documentation" or "docs" in the MR title -->
+
+<!-- Use this description template for new docs or updates to existing docs. For changing documentation location use the "Change documentation location" template -->
## What does this MR do?
@@ -10,20 +14,19 @@
Closes
-## Moving docs to a new location?
-
-Read the guidelines:
-https://docs.gitlab.com/ee/development/documentation/#changing-document-location
-
-- [ ] Make sure the old link is not removed and has its contents replaced with
- a link to the new location.
-- [ ] Make sure internal links pointing to the document in question are not broken.
-- [ ] Search and replace any links referring to the old docs in the GitLab Rails app,
- specifically under the `app/views/` and `ee/app/views` (for GitLab EE) directories.
-- [ ] Make sure to add [`redirect_from`](https://docs.gitlab.com/ee/development/documentation/index.html#redirections-for-pages-with-disqus-comments)
- to the new document if there are any Disqus comments on the old document thread.
-- [ ] If working on CE and the `ee-compat-check` jobs fails, [submit an MR to EE
- with the changes](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee) as well.
-- [ ] Ping one of the technical writers for review.
+## Author's checklist
+
+- [ ] [Apply the correct labels and milestone](https://docs.gitlab.com/ee/development/documentation/workflow.html#2-developer-s-role-in-the-documentation-process)
+- [ ] Crosslink the document from the higher-level index
+- [ ] Crosslink the document from other subject-related docs
+- [ ] Correctly apply the product [badges](https://docs.gitlab.com/ee/development/documentation/styleguide.html#product-badges) and [tiers](https://docs.gitlab.com/ee/development/documentation/styleguide.html#gitlab-versions-and-tiers)
+- [ ] [Port the MR to EE (or backport from CE)](https://docs.gitlab.com/ee/development/documentation/index.html#cherry-picking-from-ce-to-ee): _always recommended, required when the `ee-compat-check` job fails_
+
+## Review checklist
+
+- [ ] Your team's review (required)
+- [ ] PM's review (recommended, but not a blocker)
+- [ ] Technical writer's review (required)
+- [ ] Merge the EE-MR first, CE-MR afterwards
/label ~Documentation
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/GITLAB_WORKHORSE_VERSION b/GITLAB_WORKHORSE_VERSION
index 831446cbd27..09b254e90c6 100644
--- a/GITLAB_WORKHORSE_VERSION
+++ b/GITLAB_WORKHORSE_VERSION
@@ -1 +1 @@
-5.1.0
+6.0.0
diff --git a/Gemfile b/Gemfile
index d9066081f74..5666e6cebc5 100644
--- a/Gemfile
+++ b/Gemfile
@@ -180,7 +180,7 @@ gem 'rufus-scheduler', '~> 3.4'
gem 'httparty', '~> 0.13.3'
# Colored output to console
-gem 'rainbow', '~> 2.2'
+gem 'rainbow', '~> 3.0'
# Progress bar
gem 'ruby-progressbar'
diff --git a/Gemfile.lock b/Gemfile.lock
index 62c3b28f386..1aadc3fd0b6 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -691,8 +691,7 @@ GEM
activesupport (= 4.2.10)
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rainbow (2.2.2)
- rake
+ rainbow (3.0.0)
raindrops (0.18.0)
rake (12.3.1)
rb-fsevent (0.10.2)
@@ -744,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)
@@ -1134,7 +1133,7 @@ DEPENDENCIES
rails (= 4.2.10)
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 4.0.9)
- rainbow (~> 2.2)
+ rainbow (~> 3.0)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index 39305927c0f..af70e2c1939 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -701,8 +701,7 @@ GEM
method_source
rake (>= 0.8.7)
thor (>= 0.18.1, < 2.0)
- rainbow (2.2.2)
- rake
+ rainbow (3.0.0)
raindrops (0.18.0)
rake (12.3.1)
rb-fsevent (0.10.2)
@@ -1147,7 +1146,7 @@ DEPENDENCIES
rails-controller-testing
rails-deprecated_sanitizer (~> 1.0.3)
rails-i18n (~> 5.1)
- rainbow (~> 2.2)
+ rainbow (~> 3.0)
raindrops (~> 0.18)
rblineprof (~> 0.3.6)
rbtrace (~> 0.4)
diff --git a/PROCESS.md b/PROCESS.md
index 5f50d472bd7..583f36b820f 100644
--- a/PROCESS.md
+++ b/PROCESS.md
@@ -116,6 +116,11 @@ target. However, if one does and falls into either of the above categories, it's
the reviewer's responsibility to manage the above communication and assignment
on behalf of the community member.
+Every new feature or change should be shipped with its corresponding documentation
+in accordance with the
+[documentation process](https://docs.gitlab.com/ee/development/documentation/workflow.html)
+and [structure](https://docs.gitlab.com/ee/development/documentation/structure.html).
+
#### What happens if these deadlines are missed?
If a small or large feature is _not_ with a maintainer or reviewer by the
@@ -141,14 +146,9 @@ and to prevent any last minute surprises.
### On the 7th
-Merge requests should still be complete, following the
-[definition of done][done]. The single exception is documentation, and this can
-only be left until after the freeze if:
+Merge requests should still be complete, following the [definition of done][done].
-* There is a follow-up issue to add documentation.
-* It is assigned to the person writing documentation for this feature, and they
- are aware of it.
-* It is in the correct milestone, with the ~Deliverable label.
+#### Feature merge requests
If a merge request is not ready, but the developers and Product Manager
responsible for the feature think it is essential that it is in the release,
@@ -164,6 +164,23 @@ information, see
[Automatic CE->EE merge][automatic_ce_ee_merge] and
[Guidelines for implementing Enterprise Edition features][ee_features].
+#### Documentation merge requests
+
+Documentation is part of the product and must be shipped with the feature.
+
+The single exception for the feature freeze is documentation, and it can
+be left to be **merged up to the 14th** if:
+
+* There is a follow-up issue to add documentation.
+* It is assigned to the developer writing documentation for this feature, and they
+ are aware of it.
+* It is in the correct milestone, with the labels ~Documentation, ~Deliverable,
+~missed-deliverable, and "pick into X.Y" applied.
+* It must be reviewed and approved by a technical writer.
+
+For more information read the process for
+[documentation shipped late](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late).
+
### After the 7th
Once the stable branch is frozen, the only MRs that can be cherry-picked into
@@ -172,7 +189,7 @@ the stable branch are:
* Fixes for [regressions](#regressions) where the affected version `xx.x` in `regression:xx.x` is the current release. See [Managing bugs](#managing-bugs) section.
* Fixes for security issues
* Fixes or improvements to automated QA scenarios
-* Documentation updates for changes in the same release
+* [Documentation updates](https://docs.gitlab.com/ee/development/documentation/workflow.html#documentation-shipped-late) for changes in the same release
* New or updated translations (as long as they do not touch application code)
During the feature freeze all merge requests that are meant to go into the
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/commons/polyfills.js b/app/assets/javascripts/commons/polyfills.js
index 589eeee9695..742cf490ad2 100644
--- a/app/assets/javascripts/commons/polyfills.js
+++ b/app/assets/javascripts/commons/polyfills.js
@@ -8,6 +8,7 @@ import 'core-js/fn/object/assign';
import 'core-js/fn/promise';
import 'core-js/fn/string/code-point-at';
import 'core-js/fn/string/from-code-point';
+import 'core-js/fn/string/includes';
import 'core-js/fn/symbol';
import 'core-js/es6/map';
import 'core-js/es6/weak-map';
diff --git a/app/assets/javascripts/diffs/components/app.vue b/app/assets/javascripts/diffs/components/app.vue
index 7cc4e6a2c3a..b5b05df4d34 100644
--- a/app/assets/javascripts/diffs/components/app.vue
+++ b/app/assets/javascripts/diffs/components/app.vue
@@ -114,11 +114,15 @@ export default {
this.adjustView();
},
methods: {
- ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles']),
+ ...mapActions('diffs', ['setBaseConfig', 'fetchDiffFiles', 'startRenderDiffsQueue']),
fetchData() {
- this.fetchDiffFiles().catch(() => {
- createFlash(__('Something went wrong on our end. Please try again!'));
- });
+ this.fetchDiffFiles()
+ .then(() => {
+ requestIdleCallback(this.startRenderDiffsQueue, { timeout: 1000 });
+ })
+ .catch(() => {
+ createFlash(__('Something went wrong on our end. Please try again!'));
+ });
if (!this.isNotesFetched) {
eventHub.$emit('fetchNotesData');
diff --git a/app/assets/javascripts/diffs/components/diff_file.vue b/app/assets/javascripts/diffs/components/diff_file.vue
index 7e7058d8d08..59e9ba08b8b 100644
--- a/app/assets/javascripts/diffs/components/diff_file.vue
+++ b/app/assets/javascripts/diffs/components/diff_file.vue
@@ -46,16 +46,25 @@ export default {
showExpandMessage() {
return this.isCollapsed && !this.isLoadingCollapsedDiff && !this.file.tooLarge;
},
+ showLoadingIcon() {
+ return this.isLoadingCollapsedDiff || (!this.file.renderIt && !this.isCollapsed);
+ },
},
methods: {
...mapActions('diffs', ['loadCollapsedDiff']),
handleToggle() {
const { collapsed, highlightedDiffLines, parallelDiffLines } = this.file;
- if (collapsed && !highlightedDiffLines && !parallelDiffLines.length) {
+ if (
+ collapsed &&
+ !highlightedDiffLines &&
+ parallelDiffLines !== undefined &&
+ !parallelDiffLines.length
+ ) {
this.handleLoadCollapsedDiff();
} else {
this.file.collapsed = !this.file.collapsed;
+ this.file.renderIt = true;
}
},
handleLoadCollapsedDiff() {
@@ -65,6 +74,7 @@ export default {
.then(() => {
this.isLoadingCollapsedDiff = false;
this.file.collapsed = false;
+ this.file.renderIt = true;
})
.catch(() => {
this.isLoadingCollapsedDiff = false;
@@ -121,12 +131,12 @@ export default {
</div>
<diff-content
- v-if="!isCollapsed"
+ v-if="!isCollapsed && file.renderIt"
:class="{ hidden: isCollapsed || file.tooLarge }"
:diff-file="file"
/>
<loading-icon
- v-if="isLoadingCollapsedDiff"
+ v-else-if="showLoadingIcon"
class="diff-content loading"
/>
<div
diff --git a/app/assets/javascripts/diffs/constants.js b/app/assets/javascripts/diffs/constants.js
index 2fa8367f528..f68afa44837 100644
--- a/app/assets/javascripts/diffs/constants.js
+++ b/app/assets/javascripts/diffs/constants.js
@@ -25,3 +25,6 @@ export const CONTEXT_LINE_CLASS_NAME = 'diff-expanded';
export const UNFOLD_COUNT = 20;
export const COUNT_OF_AVATARS_IN_GUTTER = 3;
export const LENGTH_OF_AVATAR_TOOLTIP = 17;
+
+export const LINES_TO_BE_RENDERED_DIRECTLY = 100;
+export const MAX_LINES_TO_BE_RENDERED = 2000;
diff --git a/app/assets/javascripts/diffs/store/actions.js b/app/assets/javascripts/diffs/store/actions.js
index 27001142257..4ab6ceb249a 100644
--- a/app/assets/javascripts/diffs/store/actions.js
+++ b/app/assets/javascripts/diffs/store/actions.js
@@ -29,6 +29,27 @@ export const fetchDiffFiles = ({ state, commit }) => {
.then(handleLocationHash);
};
+export const startRenderDiffsQueue = ({ state, commit }) => {
+ const checkItem = () => {
+ const nextFile = state.diffFiles.find(
+ file => !file.renderIt && (!file.collapsed || !file.text),
+ );
+ if (nextFile) {
+ requestAnimationFrame(() => {
+ commit(types.RENDER_FILE, nextFile);
+ });
+ requestIdleCallback(
+ () => {
+ checkItem();
+ },
+ { timeout: 1000 },
+ );
+ }
+ };
+
+ checkItem();
+};
+
export const setInlineDiffViewType = ({ commit }) => {
commit(types.SET_DIFF_VIEW_TYPE, INLINE_DIFF_VIEW_TYPE);
diff --git a/app/assets/javascripts/diffs/store/mutation_types.js b/app/assets/javascripts/diffs/store/mutation_types.js
index 2c8e1a1466f..c999d637d50 100644
--- a/app/assets/javascripts/diffs/store/mutation_types.js
+++ b/app/assets/javascripts/diffs/store/mutation_types.js
@@ -8,3 +8,4 @@ export const REMOVE_COMMENT_FORM_LINE = 'REMOVE_COMMENT_FORM_LINE';
export const ADD_CONTEXT_LINES = 'ADD_CONTEXT_LINES';
export const ADD_COLLAPSED_DIFFS = 'ADD_COLLAPSED_DIFFS';
export const EXPAND_ALL_FILES = 'EXPAND_ALL_FILES';
+export const RENDER_FILE = 'RENDER_FILE';
diff --git a/app/assets/javascripts/diffs/store/mutations.js b/app/assets/javascripts/diffs/store/mutations.js
index a98b2be89a3..0522e32c410 100644
--- a/app/assets/javascripts/diffs/store/mutations.js
+++ b/app/assets/javascripts/diffs/store/mutations.js
@@ -2,6 +2,7 @@ import Vue from 'vue';
import _ from 'underscore';
import { convertObjectPropsToCamelCase } from '~/lib/utils/common_utils';
import { findDiffFile, addLineReferences, removeMatchLine, addContextLines } from './utils';
+import { LINES_TO_BE_RENDERED_DIRECTLY, MAX_LINES_TO_BE_RENDERED } from '../constants';
import * as types from './mutation_types';
export default {
@@ -15,8 +16,48 @@ export default {
},
[types.SET_DIFF_DATA](state, data) {
+ const diffData = convertObjectPropsToCamelCase(data, { deep: true });
+ let showingLines = 0;
+ const filesLength = diffData.diffFiles.length;
+ let i;
+ for (i = 0; i < filesLength; i += 1) {
+ const file = diffData.diffFiles[i];
+ if (file.parallelDiffLines) {
+ const linesLength = file.parallelDiffLines.length;
+ let u = 0;
+ for (u = 0; u < linesLength; u += 1) {
+ const line = file.parallelDiffLines[u];
+ if (line.left) delete line.left.text;
+ if (line.right) delete line.right.text;
+ }
+ }
+
+ if (file.highlightedDiffLines) {
+ const linesLength = file.highlightedDiffLines.length;
+ let u;
+ for (u = 0; u < linesLength; u += 1) {
+ const line = file.highlightedDiffLines[u];
+ delete line.text;
+ }
+ }
+
+ if (file.highlightedDiffLines) {
+ showingLines += file.parallelDiffLines.length;
+ }
+ Object.assign(file, {
+ renderIt: showingLines < LINES_TO_BE_RENDERED_DIRECTLY,
+ collapsed: file.text && showingLines > MAX_LINES_TO_BE_RENDERED,
+ });
+ }
+
Object.assign(state, {
- ...convertObjectPropsToCamelCase(data, { deep: true }),
+ ...diffData,
+ });
+ },
+
+ [types.RENDER_FILE](state, file) {
+ Object.assign(file, {
+ renderIt: true,
});
},
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/ide/stores/mutations.js b/app/assets/javascripts/ide/stores/mutations.js
index 1eda5768709..56a8d9430c7 100644
--- a/app/assets/javascripts/ide/stores/mutations.js
+++ b/app/assets/javascripts/ide/stores/mutations.js
@@ -200,6 +200,7 @@ export default {
},
[types.DELETE_ENTRY](state, path) {
const entry = state.entries[path];
+ const { tempFile = false } = entry;
const parent = entry.parentPath
? state.entries[entry.parentPath]
: state.trees[`${state.currentProjectId}/${state.currentBranchId}`];
@@ -209,7 +210,11 @@ export default {
parent.tree = parent.tree.filter(f => f.path !== entry.path);
if (entry.type === 'blob') {
- state.changedFiles = state.changedFiles.concat(entry);
+ if (tempFile) {
+ state.changedFiles = state.changedFiles.filter(f => f.path !== path);
+ } else {
+ state.changedFiles = state.changedFiles.concat(entry);
+ }
}
},
[types.RENAME_ENTRY](state, { path, name, entryPath = null }) {
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/artifacts_block.vue b/app/assets/javascripts/jobs/components/artifacts_block.vue
new file mode 100644
index 00000000000..525c5eec91a
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/artifacts_block.vue
@@ -0,0 +1,98 @@
+<script>
+ import TimeagoTooltiop from '~/vue_shared/components/time_ago_tooltip.vue';
+
+ export default {
+ components: {
+ TimeagoTooltiop,
+ },
+ props: {
+ // @build.artifacts_expired?
+ haveArtifactsExpired: {
+ type: Boolean,
+ required: true,
+ },
+ // @build.has_expiring_artifacts?
+ willArtifactsExpire: {
+ type: Boolean,
+ required: true,
+ },
+ expireAt: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ keepArtifactsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ downloadArtifactsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ browseArtifactsPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ },
+ };
+</script>
+<template>
+ <div class="block">
+ <div class="title">
+ {{ s__('Job|Job artifacts') }}
+ </div>
+
+ <p
+ v-if="haveArtifactsExpired"
+ class="js-artifacts-removed build-detail-row"
+ >
+ {{ s__('Job|The artifacts were removed') }}
+ </p>
+ <p
+ v-else-if="willArtifactsExpire"
+ class="js-artifacts-will-be-removed build-detail-row"
+ >
+ {{ s__('Job|The artifacts will be removed') }}
+ </p>
+
+ <timeago-tooltiop
+ v-if="expireAt"
+ :time="expireAt"
+ />
+
+ <div
+ class="btn-group d-flex"
+ role="group"
+ >
+ <a
+ v-if="keepArtifactsPath"
+ :href="keepArtifactsPath"
+ class="js-keep-artifacts btn btn-sm btn-default"
+ data-method="post"
+ >
+ {{ s__('Job|Keep') }}
+ </a>
+
+ <a
+ v-if="downloadArtifactsPath"
+ :href="downloadArtifactsPath"
+ class="js-download-artifacts btn btn-sm btn-default"
+ download
+ rel="nofollow"
+ >
+ {{ s__('Job|Download') }}
+ </a>
+
+ <a
+ v-if="browseArtifactsPath"
+ :href="browseArtifactsPath"
+ class="js-browse-artifacts btn btn-sm btn-default"
+ >
+ {{ s__('Job|Browse') }}
+ </a>
+ </div>
+ </div>
+</template>
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/empty_state.vue b/app/assets/javascripts/jobs/components/empty_state.vue
new file mode 100644
index 00000000000..4faf08387fb
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/empty_state.vue
@@ -0,0 +1,76 @@
+<script>
+ export default {
+ props: {
+ illustrationPath: {
+ type: String,
+ required: true,
+ },
+ illustrationSizeClass: {
+ type: String,
+ required: true,
+ },
+ title: {
+ type: String,
+ required: true,
+ },
+ content: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ action: {
+ type: Object,
+ required: false,
+ default: null,
+ validator(value) {
+ return (
+ value === null ||
+ (Object.prototype.hasOwnProperty.call(value, 'link') &&
+ Object.prototype.hasOwnProperty.call(value, 'method') &&
+ Object.prototype.hasOwnProperty.call(value, 'title'))
+ );
+ },
+ },
+ },
+ };
+</script>
+<template>
+ <div class="row empty-state">
+ <div class="col-12">
+ <div
+ :class="illustrationSizeClass"
+ class="svg-content"
+ >
+ <img :src="illustrationPath" />
+ </div>
+ </div>
+
+ <div class="col-12">
+ <div class="text-content">
+ <h4 class="js-job-empty-state-title text-center">
+ {{ title }}
+ </h4>
+
+ <p
+ v-if="content"
+ class="js-job-empty-state-content"
+ >
+ {{ content }}
+ </p>
+
+ <div
+ v-if="action"
+ class="text-center"
+ >
+ <a
+ :href="action.link"
+ :data-method="action.method"
+ class="js-job-empty-state-action btn btn-primary"
+ >
+ {{ action.title }}
+ </a>
+ </div>
+ </div>
+ </div>
+ </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/application_settings/index.js b/app/assets/javascripts/pages/admin/application_settings/index.js
index 48d75f5443b..47bd70537f1 100644
--- a/app/assets/javascripts/pages/admin/application_settings/index.js
+++ b/app/assets/javascripts/pages/admin/application_settings/index.js
@@ -1,6 +1,8 @@
import initSettingsPanels from '~/settings_panels';
+import projectSelect from '~/project_select';
document.addEventListener('DOMContentLoaded', () => {
// Initialize expandable settings panels
initSettingsPanels();
+ projectSelect();
});
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/project_select.js b/app/assets/javascripts/project_select.js
index bce7556bd40..6f3b32f8eea 100644
--- a/app/assets/javascripts/project_select.js
+++ b/app/assets/javascripts/project_select.js
@@ -14,6 +14,7 @@ export default function projectSelect() {
this.orderBy = $(select).data('orderBy') || 'id';
this.withIssuesEnabled = $(select).data('withIssuesEnabled');
this.withMergeRequestsEnabled = $(select).data('withMergeRequestsEnabled');
+ this.allowClear = $(select).data('allowClear') || false;
placeholder = "Search for project";
if (this.includeGroups) {
@@ -71,6 +72,13 @@ export default function projectSelect() {
text: function (project) {
return project.name_with_namespace || project.name;
},
+
+ initSelection: function(el, callback) {
+ return Api.project(el.val()).then(({ data }) => callback(data));
+ },
+
+ allowClear: this.allowClear,
+
dropdownCssClass: "ajax-project-dropdown"
});
if (simpleFilter) return select;
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/bootstrap_migration.scss b/app/assets/stylesheets/bootstrap_migration.scss
index 056d4b7207a..e8e707cf90c 100644
--- a/app/assets/stylesheets/bootstrap_migration.scss
+++ b/app/assets/stylesheets/bootstrap_migration.scss
@@ -85,7 +85,7 @@ strong {
}
a {
- color: $gl-link-color;
+ color: $blue-600;
}
hr {
diff --git a/app/assets/stylesheets/framework/buttons.scss b/app/assets/stylesheets/framework/buttons.scss
index ea4798fcefd..0dc7aa4ef68 100644
--- a/app/assets/stylesheets/framework/buttons.scss
+++ b/app/assets/stylesheets/framework/buttons.scss
@@ -434,7 +434,7 @@
&:hover,
&:active,
&:focus {
- color: $gl-link-color;
+ color: $blue-600;
text-decoration: none;
}
}
@@ -445,7 +445,7 @@
&:hover,
&:active,
&:focus {
- color: $gl-link-color;
+ color: $blue-600;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index af17210f341..48a87ea8616 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -114,7 +114,11 @@ hr {
.item-title { font-weight: $gl-font-weight-bold; }
.author-link {
- color: $gl-link-color;
+ color: $blue-600;
+}
+
+.author-link:hover {
+ text-decoration: none;
}
.back-link {
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 00eac1688f2..54882633fea 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -286,19 +286,19 @@ span.idiff {
.new-file {
a {
- color: $gl-text-green;
+ color: $green-600;
}
}
.renamed-file {
a {
- color: $gl-text-orange;
+ color: $orange-600;
}
}
.deleted-file {
a {
- color: $gl-text-red;
+ color: $red-500;
}
}
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index e4bcb92876d..7a4c3914fb0 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -16,10 +16,10 @@
color: $gl-text-color;
a {
- color: $gl-link-color;
+ color: $blue-600;
&:hover {
- color: $gl-link-hover-color;
+ color: $blue-800;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/framework/forms.scss b/app/assets/stylesheets/framework/forms.scss
index 437fcff5c62..a70eece8f68 100644
--- a/app/assets/stylesheets/framework/forms.scss
+++ b/app/assets/stylesheets/framework/forms.scss
@@ -170,7 +170,7 @@ label {
}
.form-control::-webkit-input-placeholder {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
}
.input-group {
diff --git a/app/assets/stylesheets/framework/markdown_area.scss b/app/assets/stylesheets/framework/markdown_area.scss
index 7290a174668..d8391b59a8c 100644
--- a/app/assets/stylesheets/framework/markdown_area.scss
+++ b/app/assets/stylesheets/framework/markdown_area.scss
@@ -179,7 +179,7 @@
&:hover,
&:focus {
svg {
- fill: $gl-link-color;
+ fill: $blue-600;
}
}
}
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index 473ca408c04..eccc814b747 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -180,7 +180,7 @@
}
a > code {
- color: $gl-link-color;
+ color: $blue-600;
}
dd {
@@ -423,25 +423,25 @@ h4 {
input,
textarea {
&::-webkit-input-placeholder {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
}
// support firefox 19+ vendor prefix
&::-moz-placeholder {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
opacity: 1; // FF defaults to 0.54
}
// scss-lint:disable PseudoElement
// support Edge vendor prefix
&::-ms-input-placeholder {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
}
// scss-lint:disable PseudoElement
// support IE vendor prefix
&:-ms-input-placeholder {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
}
}
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index d2ea314f176..866cb88ba5b 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -195,19 +195,10 @@ $gl-text-color-quaternary: #d6d6d6;
$gl-text-color-inverted: rgba(255, 255, 255, 1);
$gl-text-color-secondary-inverted: rgba(255, 255, 255, 0.85);
$gl-text-color-disabled: #919191;
-$gl-text-green: $green-600;
-$gl-text-green-hover: $green-700;
-$gl-text-red: $red-500;
-$gl-text-orange: $orange-600;
-$gl-link-color: $blue-600;
-$gl-link-hover-color: $blue-800;
$gl-grayish-blue: #7f8fa4;
-$gl-gray: $gl-text-color;
$gl-gray-dark: #313236;
$gl-gray-light: #5c5c5c;
$gl-header-color: #4c4e54;
-$gl-header-nav-hover-color: #434343;
-$placeholder-text-color: $gl-text-color-tertiary;
/*
* Lists
@@ -226,7 +217,7 @@ $list-warning-row-color: $orange-700;
/*
* Markdown
*/
-$md-link-color: $gl-link-color;
+$md-link-color: $blue-600;
$md-area-border: #ddd;
/*
@@ -583,8 +574,8 @@ $commit-message-text-area-bg: rgba(0, 0, 0, 0);
$common-gray: $gl-text-color;
$common-gray-light: #bbb;
$common-gray-dark: #444;
-$common-red: $gl-text-red;
-$common-green: $gl-text-green;
+$common-red: $red-500;
+$common-green: $green-600;
/*
* Editor
diff --git a/app/assets/stylesheets/framework/zen.scss b/app/assets/stylesheets/framework/zen.scss
index dbd3144b9b4..f2d296fb875 100644
--- a/app/assets/stylesheets/framework/zen.scss
+++ b/app/assets/stylesheets/framework/zen.scss
@@ -44,7 +44,7 @@
color: $gl-text-color-secondary;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/page_bundles/ide.scss b/app/assets/stylesheets/page_bundles/ide.scss
index 2b8163b8c68..eac1345742d 100644
--- a/app/assets/stylesheets/page_bundles/ide.scss
+++ b/app/assets/stylesheets/page_bundles/ide.scss
@@ -1158,7 +1158,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
}
a {
- color: $gl-link-color;
+ color: $blue-600;
}
}
@@ -1293,6 +1293,7 @@ $ide-tree-text-start: $ide-activity-bar-width + $ide-tree-padding;
&.build-page .top-bar {
top: 0;
+ height: auto;
font-size: 12px;
border-top-right-radius: $border-radius-default;
}
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index a68b47b1d02..91f470ca709 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -225,7 +225,7 @@
outline: 0;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
}
}
diff --git a/app/assets/stylesheets/pages/builds.scss b/app/assets/stylesheets/pages/builds.scss
index e8158cd7f6b..1696d18584d 100644
--- a/app/assets/stylesheets/pages/builds.scss
+++ b/app/assets/stylesheets/pages/builds.scss
@@ -221,7 +221,7 @@
color: $gl-text-color;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
text-decoration: none;
}
}
diff --git a/app/assets/stylesheets/pages/commits.scss b/app/assets/stylesheets/pages/commits.scss
index bce83bf0dd0..10764e0f3df 100644
--- a/app/assets/stylesheets/pages/commits.scss
+++ b/app/assets/stylesheets/pages/commits.scss
@@ -279,7 +279,7 @@
}
&.autodevops-link {
- color: $gl-link-color;
+ color: $blue-600;
}
}
@@ -321,7 +321,7 @@
}
.commit-sha {
- color: $gl-link-color;
+ color: $blue-600;
}
.commit-row-message {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index e2c0a7a6225..bba9f38d3dd 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -360,7 +360,7 @@
}
.commit-sha {
- color: $gl-link-color;
+ color: $blue-600;
line-height: 1.3;
vertical-align: top;
font-weight: $gl-font-weight-normal;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 591e21243ed..47778110bae 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -511,13 +511,13 @@
padding: 0;
background-color: transparent;
border: 0;
- color: $gl-link-color;
+ color: $blue-600;
font-weight: $gl-font-weight-bold;
&:hover,
&:focus {
outline: none;
- color: $gl-link-hover-color;
+ color: $blue-800;
}
.caret-icon {
diff --git a/app/assets/stylesheets/pages/environments.scss b/app/assets/stylesheets/pages/environments.scss
index 8a074017344..179c0964567 100644
--- a/app/assets/stylesheets/pages/environments.scss
+++ b/app/assets/stylesheets/pages/environments.scss
@@ -479,10 +479,10 @@
.deploy-info-text-link {
font-family: $monospace-font;
- fill: $gl-link-color;
+ fill: $blue-600;
&:hover {
- fill: $gl-link-hover-color;
+ fill: $blue-800;
}
}
diff --git a/app/assets/stylesheets/pages/graph.scss b/app/assets/stylesheets/pages/graph.scss
index 49d8a5d959b..22fce893fd7 100644
--- a/app/assets/stylesheets/pages/graph.scss
+++ b/app/assets/stylesheets/pages/graph.scss
@@ -24,11 +24,11 @@
}
.graph-additions {
- color: $gl-text-green;
+ color: $green-600;
}
.graph-deletions {
- color: $gl-text-red;
+ color: $red-500;
}
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index 8e78d9f65eb..d16a63d009a 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -141,7 +141,7 @@
color: inherit;
&:hover {
- color: $gl-link-hover-color;
+ color: $blue-800;
.avatar {
border-color: rgba($avatar-border, .2);
@@ -241,7 +241,7 @@
&:hover {
text-decoration: underline;
- color: $gl-link-hover-color;
+ color: $blue-800;
}
}
}
@@ -329,7 +329,7 @@
}
.btn-secondary-hover-link:hover {
- color: $gl-link-color;
+ color: $blue-600;
}
.sidebar-collapsed-icon {
@@ -448,8 +448,8 @@
}
.todo-undone {
- color: $gl-link-color;
- fill: $gl-link-color;
+ color: $blue-600;
+ fill: $blue-600;
}
.author {
diff --git a/app/assets/stylesheets/pages/issues.scss b/app/assets/stylesheets/pages/issues.scss
index 212e5979273..0f95fb911e1 100644
--- a/app/assets/stylesheets/pages/issues.scss
+++ b/app/assets/stylesheets/pages/issues.scss
@@ -157,7 +157,7 @@ ul.related-merge-requests > li {
.issuable-email-modal-btn {
padding: 0;
- color: $gl-link-color;
+ color: $blue-600;
background-color: transparent;
border: 0;
outline: 0;
@@ -190,7 +190,7 @@ ul.related-merge-requests > li {
.create-mr-dropdown-wrap {
.ref::selection {
- color: $placeholder-text-color;
+ color: $gl-text-color-tertiary;
}
.dropdown {
diff --git a/app/assets/stylesheets/pages/labels.scss b/app/assets/stylesheets/pages/labels.scss
index b25dc4f419a..d32943fceec 100644
--- a/app/assets/stylesheets/pages/labels.scss
+++ b/app/assets/stylesheets/pages/labels.scss
@@ -114,7 +114,7 @@
}
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
&.remove-row {
color: $gl-danger;
@@ -343,10 +343,10 @@
&.remove-row {
&:hover {
- color: $gl-text-red;
+ color: $red-500;
svg {
- fill: $gl-text-red;
+ fill: $red-500;
}
}
}
diff --git a/app/assets/stylesheets/pages/milestone.scss b/app/assets/stylesheets/pages/milestone.scss
index 46437ce5841..1e92582d6d9 100644
--- a/app/assets/stylesheets/pages/milestone.scss
+++ b/app/assets/stylesheets/pages/milestone.scss
@@ -30,7 +30,7 @@
.milestone-progress {
a {
- color: $gl-link-color;
+ color: $blue-600;
}
}
diff --git a/app/assets/stylesheets/pages/note_form.scss b/app/assets/stylesheets/pages/note_form.scss
index 8acd64ca1a1..4f861d43f55 100644
--- a/app/assets/stylesheets/pages/note_form.scss
+++ b/app/assets/stylesheets/pages/note_form.scss
@@ -306,7 +306,7 @@
&:hover,
&:focus {
- color: $gl-link-color;
+ color: $blue-600;
outline: 0;
}
@@ -424,7 +424,7 @@
.uploading-error-icon,
.uploading-error-message {
- color: $gl-text-red;
+ color: $red-500;
}
.uploading-error-message {
@@ -443,7 +443,7 @@
.attach-new-file,
.button-attach-file,
.retry-uploading-link {
- color: $gl-link-color;
+ color: $blue-600;
padding: 0;
background: none;
border: 0;
@@ -452,5 +452,5 @@
}
.markdown-selector {
- color: $gl-link-color;
+ color: $blue-600;
}
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index c369d89d63c..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
@@ -210,7 +207,7 @@ ul.notes {
}
a {
- color: $gl-link-color;
+ color: $blue-600;
}
p {
@@ -253,14 +250,14 @@ ul.notes {
overflow: hidden;
.system-note-commit-list-toggler {
- color: $gl-link-color;
+ color: $blue-600;
padding: 10px 0 0;
cursor: pointer;
position: relative;
z-index: 2;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
text-decoration: underline;
}
}
@@ -390,7 +387,7 @@ ul.notes {
color: inherit;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
}
&:focus,
@@ -451,7 +448,7 @@ ul.notes {
.discussion-headline-light {
a {
- color: $gl-link-color;
+ color: $blue-600;
}
}
@@ -560,12 +557,12 @@ ul.notes {
&:hover,
&.is-active {
.danger-highlight {
- color: $gl-text-red;
+ color: $red-500;
}
.link-highlight {
- color: $gl-link-color;
- fill: $gl-link-color;
+ color: $blue-600;
+ fill: $blue-600;
}
.award-control-icon-neutral {
@@ -597,13 +594,13 @@ ul.notes {
transition: color 0.1s linear;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
}
&:focus {
text-decoration: underline;
outline: none;
- color: $gl-link-color;
+ color: $blue-600;
}
.fa {
@@ -673,7 +670,7 @@ ul.notes {
}
a {
- color: $gl-link-color;
+ color: $blue-600;
}
}
@@ -759,16 +756,16 @@ ul.notes {
&:not(.is-disabled) {
&:hover,
&:focus {
- color: $gl-text-green;
+ color: $green-600;
}
}
&.is-active {
- color: $gl-text-green;
+ color: $green-600;
&:hover,
&:focus {
- color: $gl-text-green-hover;
+ color: $green-700;
}
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index b68c89c25d8..ad057ed3c83 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -175,7 +175,7 @@
}
.commit-sha {
- color: $gl-link-color;
+ color: $blue-600;
}
.badge {
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index 6d9f415e869..fffb440027c 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -388,7 +388,7 @@
line-height: $gl-btn-line-height;
&:hover {
- color: $gl-link-color;
+ color: $blue-600;
}
}
}
@@ -961,7 +961,7 @@ pre.light-well {
margin-left: 5px;
&.is-done {
- color: $gl-text-green;
+ color: $green-600;
}
}
diff --git a/app/assets/stylesheets/pages/search.scss b/app/assets/stylesheets/pages/search.scss
index c9405004c38..5b3a468cd1c 100644
--- a/app/assets/stylesheets/pages/search.scss
+++ b/app/assets/stylesheets/pages/search.scss
@@ -259,6 +259,6 @@ input[type='checkbox']:hover {
&:hover,
&:focus {
- color: $gl-link-color;
+ color: $blue-600;
}
}
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/finders/license_template_finder.rb b/app/finders/license_template_finder.rb
new file mode 100644
index 00000000000..fad33f0eca2
--- /dev/null
+++ b/app/finders/license_template_finder.rb
@@ -0,0 +1,36 @@
+# LicenseTemplateFinder
+#
+# Used to find license templates, which may come from a variety of external
+# sources
+#
+# Arguments:
+# popular: boolean. When set to true, only "popular" licenses are shown. When
+# false, all licenses except popular ones are shown. When nil (the
+# default), *all* licenses will be shown.
+class LicenseTemplateFinder
+ attr_reader :params
+
+ def initialize(params = {})
+ @params = params
+ end
+
+ def execute
+ Licensee::License.all(featured: popular_only?).map do |license|
+ LicenseTemplate.new(
+ id: license.key,
+ name: license.name,
+ nickname: license.nickname,
+ category: (license.featured? ? :Popular : :Other),
+ content: license.content,
+ url: license.url,
+ meta: license.meta
+ )
+ end
+ end
+
+ private
+
+ def popular_only?
+ params.fetch(:popular, nil)
+ end
+end
diff --git a/app/helpers/application_settings_helper.rb b/app/helpers/application_settings_helper.rb
index 2bdf2c2c120..1e05f07e676 100644
--- a/app/helpers/application_settings_helper.rb
+++ b/app/helpers/application_settings_helper.rb
@@ -254,6 +254,7 @@ module ApplicationSettingsHelper
:usage_ping_enabled,
:instance_statistics_visibility_private,
:user_default_external,
+ :user_show_add_ssh_key_message,
:user_oauth_applications,
:version_check_enabled,
:web_ide_clientside_preview_enabled
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 7eb45ddd117..b61cbd5418a 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -182,12 +182,14 @@ module BlobHelper
def licenses_for_select
return @licenses_for_select if defined?(@licenses_for_select)
- licenses = Licensee::License.all
+ grouped_licenses = LicenseTemplateFinder.new.execute.group_by(&:category)
+ categories = grouped_licenses.keys
- @licenses_for_select = {
- Popular: licenses.select(&:featured).map { |license| { name: license.name, id: license.key } },
- Other: licenses.reject(&:featured).map { |license| { name: license.name, id: license.key } }
- }
+ @licenses_for_select = categories.each_with_object({}) do |category, hash|
+ hash[category] = grouped_licenses[category].map do |license|
+ { name: license.name, id: license.id }
+ end
+ end
end
def ref_project
diff --git a/app/helpers/button_helper.rb b/app/helpers/button_helper.rb
index 0171a880164..7adc882bc47 100644
--- a/app/helpers/button_helper.rb
+++ b/app/helpers/button_helper.rb
@@ -73,7 +73,11 @@ module ButtonHelper
end
def ssh_clone_button(project, append_link: true)
- dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile") if current_user.try(:require_ssh_key?)
+ if Gitlab::CurrentSettings.user_show_add_ssh_key_message? &&
+ current_user.try(:require_ssh_key?)
+ dropdown_description = _("You won't be able to pull or push project code via SSH until you add an SSH key to your profile")
+ end
+
append_url = project.ssh_url_to_repo if append_link
dropdown_item_with_description('SSH', dropdown_description, href: append_url)
diff --git a/app/helpers/icons_helper.rb b/app/helpers/icons_helper.rb
index 41084ec686f..a8a10c98d69 100644
--- a/app/helpers/icons_helper.rb
+++ b/app/helpers/icons_helper.rb
@@ -62,6 +62,8 @@ module IconsHelper
names = "key"
when "two-factor"
names = "key"
+ when "google_oauth2"
+ names = "google"
end
options.include?(:base) ? fa_stacked_icon(names, options) : fa_icon(names, options)
diff --git a/app/helpers/projects_helper.rb b/app/helpers/projects_helper.rb
index aaf9dff43ee..6b4079b4113 100644
--- a/app/helpers/projects_helper.rb
+++ b/app/helpers/projects_helper.rb
@@ -192,7 +192,10 @@ module ProjectsHelper
end
def show_no_ssh_key_message?
- cookies[:hide_no_ssh_message].blank? && !current_user.hide_no_ssh_key && current_user.require_ssh_key?
+ Gitlab::CurrentSettings.user_show_add_ssh_key_message? &&
+ cookies[:hide_no_ssh_message].blank? &&
+ !current_user.hide_no_ssh_key &&
+ current_user.require_ssh_key?
end
def show_no_password_message?
diff --git a/app/mailers/abuse_report_mailer.rb b/app/mailers/abuse_report_mailer.rb
index fe5f68ba3d5..e032f568913 100644
--- a/app/mailers/abuse_report_mailer.rb
+++ b/app/mailers/abuse_report_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class AbuseReportMailer < BaseMailer
def notify(abuse_report_id)
return unless deliverable?
diff --git a/app/mailers/base_mailer.rb b/app/mailers/base_mailer.rb
index 654468bc7fe..5fd209c4761 100644
--- a/app/mailers/base_mailer.rb
+++ b/app/mailers/base_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class BaseMailer < ActionMailer::Base
around_action :render_with_default_locale
diff --git a/app/mailers/devise_mailer.rb b/app/mailers/devise_mailer.rb
index 962570a0efd..7aa75ee30e6 100644
--- a/app/mailers/devise_mailer.rb
+++ b/app/mailers/devise_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeviseMailer < Devise::Mailer
default from: "#{Gitlab.config.gitlab.email_display_name} <#{Gitlab.config.gitlab.email_from}>"
default reply_to: Gitlab.config.gitlab.email_reply_to
@@ -9,8 +11,9 @@ class DeviseMailer < Devise::Mailer
protected
def subject_for(key)
- subject = super
- subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
- subject
+ subject = [super]
+ subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present?
+
+ subject.join(' | ')
end
end
diff --git a/app/mailers/email_rejection_mailer.rb b/app/mailers/email_rejection_mailer.rb
index 76db31a4c45..45fc5a6c383 100644
--- a/app/mailers/email_rejection_mailer.rb
+++ b/app/mailers/email_rejection_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailRejectionMailer < BaseMailer
def rejection(reason, original_raw, can_retry = false)
@reason = reason
diff --git a/app/mailers/emails/issues.rb b/app/mailers/emails/issues.rb
index 392cc0bee03..c8b1ab5033a 100644
--- a/app/mailers/emails/issues.rb
+++ b/app/mailers/emails/issues.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Issues
def new_issue_email(recipient_id, issue_id, reason = nil)
diff --git a/app/mailers/emails/members.rb b/app/mailers/emails/members.rb
index 75cf56a51f2..91dfdf58982 100644
--- a/app/mailers/emails/members.rb
+++ b/app/mailers/emails/members.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Members
extend ActiveSupport::Concern
diff --git a/app/mailers/emails/merge_requests.rb b/app/mailers/emails/merge_requests.rb
index 70509e9066d..70f65d4e58d 100644
--- a/app/mailers/emails/merge_requests.rb
+++ b/app/mailers/emails/merge_requests.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module MergeRequests
def new_merge_request_email(recipient_id, merge_request_id, reason = nil)
diff --git a/app/mailers/emails/notes.rb b/app/mailers/emails/notes.rb
index d9a6fe2a41e..d3284e90568 100644
--- a/app/mailers/emails/notes.rb
+++ b/app/mailers/emails/notes.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Notes
def note_commit_email(recipient_id, note_id)
diff --git a/app/mailers/emails/pages_domains.rb b/app/mailers/emails/pages_domains.rb
index 0027dfdc36b..ce449237ef6 100644
--- a/app/mailers/emails/pages_domains.rb
+++ b/app/mailers/emails/pages_domains.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module PagesDomains
def pages_domain_enabled_email(domain, recipient)
diff --git a/app/mailers/emails/pipelines.rb b/app/mailers/emails/pipelines.rb
index f9f45ab987b..31e183640ad 100644
--- a/app/mailers/emails/pipelines.rb
+++ b/app/mailers/emails/pipelines.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Pipelines
def pipeline_success_email(pipeline, recipients)
@@ -39,10 +41,10 @@ module Emails
end
def pipeline_subject(status)
- commit = @pipeline.short_sha
- commit << " in #{@merge_request.to_reference}" if @merge_request
+ commit = [@pipeline.short_sha]
+ commit << "in #{@merge_request.to_reference}" if @merge_request
- subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit)
+ subject("Pipeline ##{@pipeline.id} has #{status} for #{@pipeline.ref}", commit.join(' '))
end
end
end
diff --git a/app/mailers/emails/profile.rb b/app/mailers/emails/profile.rb
index 4f5edeb9bda..40d7b9ccd7a 100644
--- a/app/mailers/emails/profile.rb
+++ b/app/mailers/emails/profile.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Profile
def new_user_email(user_id, token = nil)
diff --git a/app/mailers/emails/projects.rb b/app/mailers/emails/projects.rb
index 761d873c01c..d7e6c2ba7b2 100644
--- a/app/mailers/emails/projects.rb
+++ b/app/mailers/emails/projects.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
module Emails
module Projects
def project_was_moved_email(project_id, user_id, old_path_with_namespace)
diff --git a/app/mailers/notify.rb b/app/mailers/notify.rb
index 0e1e39501f5..f4eeb85270e 100644
--- a/app/mailers/notify.rb
+++ b/app/mailers/notify.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class Notify < BaseMailer
include ActionDispatch::Routing::PolymorphicRoutes
include GitlabRoutingHelper
@@ -92,12 +94,14 @@ class Notify < BaseMailer
# >> subject('Lorem ipsum', 'Dolor sit amet')
# => "Lorem ipsum | Dolor sit amet"
def subject(*extra)
- subject = ""
- subject << "#{@project.name} | " if @project
- subject << "#{@group.name} | " if @group
- subject << extra.join(' | ') if extra.present?
- subject << " | #{Gitlab.config.gitlab.email_subject_suffix}" if Gitlab.config.gitlab.email_subject_suffix.present?
- subject
+ subject = []
+
+ subject << @project.name if @project
+ subject << @group.name if @group
+ subject.concat(extra) if extra.present?
+ subject << Gitlab.config.gitlab.email_subject_suffix if Gitlab.config.gitlab.email_subject_suffix.present?
+
+ subject.join(' | ')
end
# Return a string suitable for inclusion in the 'Message-Id' mail header.
diff --git a/app/mailers/previews/devise_mailer_preview.rb b/app/mailers/previews/devise_mailer_preview.rb
index d6588efc486..3b9ef0d3ac0 100644
--- a/app/mailers/previews/devise_mailer_preview.rb
+++ b/app/mailers/previews/devise_mailer_preview.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class DeviseMailerPreview < ActionMailer::Preview
def confirmation_instructions_for_signup
DeviseMailer.confirmation_instructions(unsaved_user, 'faketoken', {})
diff --git a/app/mailers/previews/email_rejection_mailer_preview.rb b/app/mailers/previews/email_rejection_mailer_preview.rb
index 639e8471232..402066151ef 100644
--- a/app/mailers/previews/email_rejection_mailer_preview.rb
+++ b/app/mailers/previews/email_rejection_mailer_preview.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class EmailRejectionMailerPreview < ActionMailer::Preview
def rejection
EmailRejectionMailer.rejection("some rejection reason", "From: someone@example.com\nraw email here").message
diff --git a/app/mailers/previews/notify_preview.rb b/app/mailers/previews/notify_preview.rb
index 3615cde8026..df470930e9e 100644
--- a/app/mailers/previews/notify_preview.rb
+++ b/app/mailers/previews/notify_preview.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class NotifyPreview < ActionMailer::Preview
def note_merge_request_email_for_individual_note
note_email(:note_merge_request_email) do
diff --git a/app/mailers/previews/repository_check_mailer_preview.rb b/app/mailers/previews/repository_check_mailer_preview.rb
index 19d4eab1805..834d7594719 100644
--- a/app/mailers/previews/repository_check_mailer_preview.rb
+++ b/app/mailers/previews/repository_check_mailer_preview.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryCheckMailerPreview < ActionMailer::Preview
def notify
RepositoryCheckMailer.notify(3).message
diff --git a/app/mailers/repository_check_mailer.rb b/app/mailers/repository_check_mailer.rb
index 22a9f5da646..4bcf371cfc0 100644
--- a/app/mailers/repository_check_mailer.rb
+++ b/app/mailers/repository_check_mailer.rb
@@ -1,3 +1,5 @@
+# frozen_string_literal: true
+
class RepositoryCheckMailer < BaseMailer
def notify(failed_count)
@message =
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index bbe7811841a..c77faa4b71d 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -298,7 +298,8 @@ class ApplicationSetting < ActiveRecord::Base
unique_ips_limit_time_window: 3600,
usage_ping_enabled: Settings.gitlab['usage_ping_enabled'],
instance_statistics_visibility_private: false,
- user_default_external: false
+ user_default_external: false,
+ user_show_add_ssh_key_message: true
}
end
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/ci/job_artifact.rb b/app/models/ci/job_artifact.rb
index d7c5f29be96..17b7ee4f07e 100644
--- a/app/models/ci/job_artifact.rb
+++ b/app/models/ci/job_artifact.rb
@@ -33,7 +33,7 @@ module Ci
where(file_type: types)
end
- delegate :exists?, :open, to: :file
+ delegate :filename, :exists?, :open, to: :file
enum file_type: {
archive: 1,
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/license_template.rb b/app/models/license_template.rb
new file mode 100644
index 00000000000..0ad75b27827
--- /dev/null
+++ b/app/models/license_template.rb
@@ -0,0 +1,53 @@
+class LicenseTemplate
+ PROJECT_TEMPLATE_REGEX =
+ %r{[\<\{\[]
+ (project|description|
+ one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
+ [\>\}\]]}xi.freeze
+ YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
+ FULLNAME_TEMPLATE_REGEX =
+ %r{[\<\{\[]
+ (fullname|name\sof\s(author|copyright\sowner))
+ [\>\}\]]}xi.freeze
+
+ attr_reader :id, :name, :category, :nickname, :url, :meta
+
+ alias_method :key, :id
+
+ def initialize(id:, name:, category:, content:, nickname: nil, url: nil, meta: {})
+ @id = id
+ @name = name
+ @category = category
+ @content = content
+ @nickname = nickname
+ @url = url
+ @meta = meta
+ end
+
+ def popular?
+ category == :Popular
+ end
+ alias_method :featured?, :popular?
+
+ # Returns the text of the license
+ def content
+ if @content.respond_to?(:call)
+ @content = @content.call
+ else
+ @content
+ end
+ end
+
+ # Populate placeholders in the LicenseTemplate content
+ def resolve!(project_name: nil, fullname: nil, year: Time.now.year.to_s)
+ # Ensure the string isn't shared with any other instance of LicenseTemplate
+ new_content = content.dup
+ new_content.gsub!(YEAR_TEMPLATE_REGEX, year) if year.present?
+ new_content.gsub!(PROJECT_TEMPLATE_REGEX, project_name) if project_name.present?
+ new_content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname.present?
+
+ @content = new_content
+
+ self
+ end
+end
diff --git a/app/models/namespace.rb b/app/models/namespace.rb
index b974309aeb6..0deb44d7916 100644
--- a/app/models/namespace.rb
+++ b/app/models/namespace.rb
@@ -10,6 +10,7 @@ class Namespace < ActiveRecord::Base
include Storage::LegacyNamespace
include Gitlab::SQL::Pattern
include IgnorableColumn
+ include FeatureGate
ignore_column :deleted_at
@@ -124,7 +125,6 @@ class Namespace < ActiveRecord::Base
def to_param
full_path
end
- alias_method :flipper_id, :to_param
def human_name
owner_name
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/_account_and_limit.html.haml b/app/views/admin/application_settings/_account_and_limit.html.haml
index 7c8243a7a90..622cb11010e 100644
--- a/app/views/admin/application_settings/_account_and_limit.html.haml
+++ b/app/views/admin/application_settings/_account_and_limit.html.haml
@@ -29,5 +29,11 @@
= f.check_box :user_default_external, class: 'form-check-input'
= f.label :user_default_external, class: 'form-check-label' do
Newly registered users will by default be external
+ .form-group
+ = f.label :user_show_add_ssh_key_message, 'Prompt users to upload SSH keys', class: 'label-bold'
+ .form-check
+ = f.check_box :user_show_add_ssh_key_message, class: 'form-check-input'
+ = f.label :user_show_add_ssh_key_message, class: 'form-check-label' do
+ Inform users without uploaded SSH keys that they can't push over SSH until one is added
= f.submit 'Save changes', class: 'btn btn-success'
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/application_settings/show.html.haml b/app/views/admin/application_settings/show.html.haml
index 258d50ad676..6133a7646f4 100644
--- a/app/views/admin/application_settings/show.html.haml
+++ b/app/views/admin/application_settings/show.html.haml
@@ -325,6 +325,8 @@
.settings-content
= render partial: 'repository_mirrors_form'
+= render_if_exists 'admin/application_settings/templates', expanded: expanded
+
%section.settings.as-third-party-offers.no-animate#js-third-party-offers-settings{ class: ('expanded' if expanded) }
.settings-header
%h4
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/_instructions.html.haml b/app/views/projects/mirrors/_instructions.html.haml
index 3d811be3fe3..e051f9e6331 100644
--- a/app/views/projects/mirrors/_instructions.html.haml
+++ b/app/views/projects/mirrors/_instructions.html.haml
@@ -4,7 +4,7 @@
= _('The repository must be accessible over <code>http://</code>,
<code>https://</code>, <code>ssh://</code> and <code>git://</code>.').html_safe
%li= _('Include the username in the URL if required: <code>https://username@gitlab.company.com/group/project.git</code>.').html_safe
- %li= _('The update action will time out after 15 minutes. For big repositories, use a clone/push combination.')
+ %li= _("The update action will time out after #{import_will_timeout_message(Gitlab.config.gitlab_shell.git_timeout)} minutes. For big repositories, use a clone/push combination.")
%li= _('The Git LFS objects will <strong>not</strong> be synced.').html_safe
%li
= _('This user will be the author of all events in the activity feed that are the result of an update,
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/settings/ci_cd/_form.html.haml b/app/views/projects/settings/ci_cd/_form.html.haml
index 434aed2f603..9134257b631 100644
--- a/app/views/projects/settings/ci_cd/_form.html.haml
+++ b/app/views/projects/settings/ci_cd/_form.html.haml
@@ -17,7 +17,7 @@
%h5.prepend-top-0
= _("Git strategy for pipelines")
%p
- = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code")
+ = _("Choose between <code>clone</code> or <code>fetch</code> to get the recent application code").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'git-strategy'), target: '_blank'
.form-check
= f.radio_button :build_allow_git_fetch, 'false', { class: 'form-check-input' }
@@ -47,7 +47,7 @@
= f.label :ci_config_path, _('Custom CI config path'), class: 'label-bold'
= f.text_field :ci_config_path, class: 'form-control', placeholder: '.gitlab-ci.yml'
%p.form-text.text-muted
- = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>")
+ = _("The path to CI config file. Defaults to <code>.gitlab-ci.yml</code>").html_safe
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'custom-ci-config-path'), target: '_blank'
%hr
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/views/search/results/_blob.html.haml b/app/views/search/results/_blob.html.haml
index fdcd126e7a3..a8d4d4af93a 100644
--- a/app/views/search/results/_blob.html.haml
+++ b/app/views/search/results/_blob.html.haml
@@ -1,4 +1,6 @@
- project = find_project_for_result_blob(blob)
+- return unless project
+
- file_name, blob = parse_search_result(blob)
- blob_link = project_blob_path(project, tree_join(blob.ref, file_name))
diff --git a/app/views/shared/runners/_form.html.haml b/app/views/shared/runners/_form.html.haml
index 0337680d79b..fa93307be31 100644
--- a/app/views/shared/runners/_form.html.haml
+++ b/app/views/shared/runners/_form.html.haml
@@ -1,56 +1,56 @@
= form_for runner, url: runner_form_url do |f|
= form_errors(runner)
.form-group.row
- = label :active, "Active", class: 'col-form-label col-sm-2'
+ = label :active, _("Active"), class: 'col-form-label col-sm-2'
.col-sm-10
.form-check
= f.check_box :active, { class: 'form-check-input' }
- %span.light Paused Runners don't accept new jobs
+ %label.light{ for: :runner_active }= _("Paused Runners don't accept new jobs")
.form-group.row
- = label :protected, "Protected", class: 'col-form-label col-sm-2'
+ = label :protected, _("Protected"), class: 'col-form-label col-sm-2'
.col-sm-10
.form-check
= f.check_box :access_level, { class: 'form-check-input' }, 'ref_protected', 'not_protected'
- %span.light This runner will only run on pipelines triggered on protected branches
+ %label.light{ for: :runner_access_level }= _('This runner will only run on pipelines triggered on protected branches')
.form-group.row
- = label :run_untagged, 'Run untagged jobs', class: 'col-form-label col-sm-2'
+ = label :run_untagged, _('Run untagged jobs'), class: 'col-form-label col-sm-2'
.col-sm-10
.form-check
= f.check_box :run_untagged, { class: 'form-check-input' }
- %span.light Indicates whether this runner can pick jobs without tags
+ %label.light{ for: :runner_run_untagged }= _('Indicates whether this runner can pick jobs without tags')
- unless runner.group_type?
.form-group.row
= label :locked, _('Lock to current projects'), class: 'col-form-label col-sm-2'
.col-sm-10
.form-check
= f.check_box :locked, { class: 'form-check-input' }
- %span.light= _('When a runner is locked, it cannot be assigned to other projects')
+ %label.light{ for: :runner_locked }= _('When a runner is locked, it cannot be assigned to other projects')
.form-group.row
= label_tag :token, class: 'col-form-label col-sm-2' do
- Token
+ = _('Token')
.col-sm-10
= f.text_field :token, class: 'form-control', readonly: true
.form-group.row
= label_tag :ip_address, class: 'col-form-label col-sm-2' do
- IP Address
+ = _('IP Address')
.col-sm-10
= f.text_field :ip_address, class: 'form-control', readonly: true
.form-group.row
= label_tag :description, class: 'col-form-label col-sm-2' do
- Description
+ = _('Description')
.col-sm-10
= f.text_field :description, class: 'form-control'
.form-group.row
= label_tag :maximum_timeout_human_readable, class: 'col-form-label col-sm-2' do
- Maximum job timeout
+ = _('Maximum job timeout')
.col-sm-10
= f.text_field :maximum_timeout_human_readable, class: 'form-control'
- .form-text.text-muted This timeout will take precedence when lower than Project-defined timeout
+ .form-text.text-muted= _('This timeout will take precedence when lower than Project-defined timeout')
.form-group.row
= label_tag :tag_list, class: 'col-form-label col-sm-2' do
- Tags
+ = _('Tags')
.col-sm-10
= f.text_field :tag_list, value: runner.tag_list.sort.join(', '), class: 'form-control'
- .form-text.text-muted You can setup jobs to only use Runners with specific tags. Separate tags with commas.
+ .form-text.text-muted= _('You can setup jobs to only use Runners with specific tags. Separate tags with commas.')
.form-actions
- = f.submit 'Save changes', class: 'btn btn-save'
+ = f.submit _('Save changes'), class: 'btn btn-success'
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/bin/secpick b/bin/secpick
index 5029fe57cfe..5e30c8e72c5 100755
--- a/bin/secpick
+++ b/bin/secpick
@@ -35,7 +35,9 @@ parser.parse!
abort("Missing options. Use #{$0} --help to see the list of options available".red) if options.values.include?(nil)
abort("Wrong version format #{options[:version].bold}".red) unless options[:version] =~ /\A\d*\-\d*\Z/
-branch = [BRANCH_PREFIX, options[:branch], options[:version]].join('-').freeze
+branch = "#{options[:branch]}-#{options[:version]}"
+branch.prepend("#{BRANCH_PREFIX}-") unless branch.start_with?("#{BRANCH_PREFIX}-")
+branch = branch.freeze
stable_branch = "#{BRANCH_PREFIX}-#{options[:version]}".freeze
command = "git fetch #{REMOTE} #{stable_branch} && git checkout #{stable_branch} && git pull #{REMOTE} #{stable_branch} && git checkout -B #{branch} && git cherry-pick #{options[:sha]} && git push #{REMOTE} #{branch}"
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/49905-fix-checkboxes-runners.yml b/changelogs/unreleased/49905-fix-checkboxes-runners.yml
new file mode 100644
index 00000000000..af40e5348b8
--- /dev/null
+++ b/changelogs/unreleased/49905-fix-checkboxes-runners.yml
@@ -0,0 +1,5 @@
+---
+title: Fix checkboxes on runner admin settings - The labels are now clickable
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml
new file mode 100644
index 00000000000..82423092792
--- /dev/null
+++ b/changelogs/unreleased/49953-add-user_show_add_ssh_key_message-setting.yml
@@ -0,0 +1,5 @@
+---
+title: Add ability to suppress the global "You won't be able to use SSH" message
+merge_request: 21027
+author: Ævar Arnfjörð Bjarmason
+type: added
diff --git a/changelogs/unreleased/50101-aritfacts-block.yml b/changelogs/unreleased/50101-aritfacts-block.yml
new file mode 100644
index 00000000000..435e9d9d486
--- /dev/null
+++ b/changelogs/unreleased/50101-aritfacts-block.yml
@@ -0,0 +1,5 @@
+---
+title: Creates Vue component for artifacts block on job page
+merge_request:
+author:
+type: other
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-empty-state-component.yml b/changelogs/unreleased/50101-empty-state-component.yml
new file mode 100644
index 00000000000..ee99b65d964
--- /dev/null
+++ b/changelogs/unreleased/50101-empty-state-component.yml
@@ -0,0 +1,5 @@
+---
+title: Creates empty state vue component for job view
+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/50180-fa-icon-google-audit.yml b/changelogs/unreleased/50180-fa-icon-google-audit.yml
new file mode 100644
index 00000000000..fb1771a7570
--- /dev/null
+++ b/changelogs/unreleased/50180-fa-icon-google-audit.yml
@@ -0,0 +1,5 @@
+---
+title: Show google icon in audit log
+merge_request: 21207
+author: Jan Beckmann
+type: fixed
diff --git a/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml
new file mode 100644
index 00000000000..eb20e34c466
--- /dev/null
+++ b/changelogs/unreleased/50281-js-pages-do-not-load-on-windows-8-ie-11.yml
@@ -0,0 +1,5 @@
+---
+title: Fix broken JavaScript in IE11
+merge_request: 21214
+author:
+type: fixed
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/auto-devops-gitlab-ci-glic-228.yml b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml
new file mode 100644
index 00000000000..a1625193189
--- /dev/null
+++ b/changelogs/unreleased/auto-devops-gitlab-ci-glic-228.yml
@@ -0,0 +1,5 @@
+---
+title: 'Auto-DevOps.gitlab-ci.yml: update glibc package to 2.28'
+merge_request: 21191
+author: sgerrand
+type: fixed
diff --git a/changelogs/unreleased/bvl-add-czech.yml b/changelogs/unreleased/bvl-add-czech.yml
new file mode 100644
index 00000000000..49e0e4a74b7
--- /dev/null
+++ b/changelogs/unreleased/bvl-add-czech.yml
@@ -0,0 +1,5 @@
+---
+title: Add Czech as an available language.
+merge_request: 21201
+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/expose-all-artifacts-sizes-in-jobs-api.yml b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml
new file mode 100644
index 00000000000..1453d39934b
--- /dev/null
+++ b/changelogs/unreleased/expose-all-artifacts-sizes-in-jobs-api.yml
@@ -0,0 +1,5 @@
+---
+title: Expose all artifacts sizes in jobs api
+merge_request: 20821
+author: Peter Marko
+type: added
diff --git a/changelogs/unreleased/frozen-string-enable-app-mailers.yml b/changelogs/unreleased/frozen-string-enable-app-mailers.yml
new file mode 100644
index 00000000000..2cd247ca76c
--- /dev/null
+++ b/changelogs/unreleased/frozen-string-enable-app-mailers.yml
@@ -0,0 +1,5 @@
+---
+title: Enable frozen in app/mailers/**/*.rb
+merge_request: 21147
+author: gfyoung
+type: performance
diff --git a/changelogs/unreleased/ide-delete-new-files-state.yml b/changelogs/unreleased/ide-delete-new-files-state.yml
new file mode 100644
index 00000000000..500115d19d0
--- /dev/null
+++ b/changelogs/unreleased/ide-delete-new-files-state.yml
@@ -0,0 +1,5 @@
+---
+title: Fixed IDE deleting new files creating wrong state
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/ide-job-top-bar-ui-polish.yml b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml
new file mode 100644
index 00000000000..d917c14e5f8
--- /dev/null
+++ b/changelogs/unreleased/ide-job-top-bar-ui-polish.yml
@@ -0,0 +1,5 @@
+---
+title: Improved styling of top bar in IDE job trace pane
+merge_request:
+author:
+type: changed
diff --git a/changelogs/unreleased/mk-bump-rainbow-gem.yml b/changelogs/unreleased/mk-bump-rainbow-gem.yml
new file mode 100644
index 00000000000..31c003fb4d9
--- /dev/null
+++ b/changelogs/unreleased/mk-bump-rainbow-gem.yml
@@ -0,0 +1,5 @@
+---
+title: Fix bin/secpick error and security branch prefixing
+merge_request: 21210
+author:
+type: fixed
diff --git a/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml
new file mode 100644
index 00000000000..bcf3d2c8e16
--- /dev/null
+++ b/changelogs/unreleased/n8rzz-consolidate-specs-testing-emoji-awards.yml
@@ -0,0 +1,6 @@
+---
+title: Combines emoji award spec files into single user_interacts_with_awards_in_issue_spec.rb
+ file
+merge_request: 21126
+author: Nate Geslin
+type: other
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/changelogs/unreleased/tz-mr-incremental-rendering.yml b/changelogs/unreleased/tz-mr-incremental-rendering.yml
new file mode 100644
index 00000000000..a35fa200363
--- /dev/null
+++ b/changelogs/unreleased/tz-mr-incremental-rendering.yml
@@ -0,0 +1,4 @@
+title: Incremental rendering with Vue on merge request page
+merge_request: 21063
+author:
+type: performance
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/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb
new file mode 100644
index 00000000000..e3019af2cc9
--- /dev/null
+++ b/db/migrate/20180808162000_add_user_show_add_ssh_key_message_to_application_settings.rb
@@ -0,0 +1,19 @@
+# See http://doc.gitlab.com/ce/development/migration_style_guide.html
+# for more information on how to write migrations for GitLab.
+
+class AddUserShowAddSshKeyMessageToApplicationSettings < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ # Set this constant to true if this migration requires downtime.
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default :application_settings, :user_show_add_ssh_key_message, :boolean, default: true, allow_null: false
+ end
+
+ def down
+ remove_column :application_settings, :user_show_add_ssh_key_message
+ 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 f1d8f4df3b7..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: 20180807153545) do
+ActiveRecord::Schema.define(version: 20180809195358) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -170,6 +170,7 @@ ActiveRecord::Schema.define(version: 20180807153545) do
t.boolean "hide_third_party_offers", default: false, null: false
t.boolean "instance_statistics_visibility_private", default: false, null: false
t.boolean "web_ide_clientside_preview_enabled", default: false, null: false
+ t.boolean "user_show_add_ssh_key_message", default: true, null: false
end
create_table "audit_events", force: :cascade do |t|
diff --git a/doc/README.md b/doc/README.md
index a814c787f94..4248f62c08c 100644
--- a/doc/README.md
+++ b/doc/README.md
@@ -133,6 +133,7 @@ scales to run your tests faster.
- [GitLab CI/CD](ci/README.md): Explore the features and capabilities of Continuous Integration, Continuous Delivery, and Continuous Deployment with GitLab.
- [Review Apps](ci/review_apps/index.md): Preview changes to your app right from a merge request.
- [Pipeline Graphs](ci/pipelines.md#pipeline-graphs)
+- [JUnit test reports](ci/junit_test_reports.md)
### Package
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/operations/ssh_certificates.md b/doc/administration/operations/ssh_certificates.md
index 8968afba01b..9edccd25ced 100644
--- a/doc/administration/operations/ssh_certificates.md
+++ b/doc/administration/operations/ssh_certificates.md
@@ -163,3 +163,20 @@ Such a restriction can currently be hacked in by e.g. providing a
custom `AuthorizedKeysCommand` which checks if the discovered key-ID
returned from `gitlab-shell-authorized-keys-check` is a deploy key or
not (all non-deploy keys should be refused).
+
+## Disabling the global warning about users lacking SSH keys
+
+By default GitLab will show a "You won't be able to pull or push
+project code via SSH" warning to users who have not uploaded an SSH
+key to their profile.
+
+This is counterproductive when using SSH certificates, since users
+aren't expected to upload their own keys.
+
+To disable this warning globally, go to "Application settings ->
+Account and limit settings" and disable the "Show user add SSH key
+message" setting.
+
+This setting was added specifically for use with SSH certificates, but
+can be turned off without using them if you'd like to hide the warning
+for some other reason.
diff --git a/doc/api/jobs.md b/doc/api/jobs.md
index 9a950097675..4bf65a8fafd 100644
--- a/doc/api/jobs.md
+++ b/doc/api/jobs.md
@@ -33,7 +33,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"id": 6,
@@ -45,6 +44,7 @@ Example of response
"status": "pending"
},
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:24.729Z",
@@ -82,6 +82,12 @@ Example of response
"filename": "artifacts.zip",
"size": 1000
},
+ "artifacts": [
+ {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"},
+ {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"},
+ {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"},
+ {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"}
+ ],
"finished_at": "2015-12-24T17:54:27.895Z",
"artifacts_expire_at": "2016-01-23T17:54:27.895Z",
"id": 7,
@@ -93,6 +99,7 @@ Example of response
"status": "pending"
},
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:27.722Z",
@@ -151,7 +158,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.727Z",
- "artifacts_file": null,
"finished_at": "2015-12-24T17:54:24.921Z",
"artifacts_expire_at": "2016-01-23T17:54:24.921Z",
"id": 6,
@@ -163,6 +169,7 @@ Example of response
"status": "pending"
},
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:24.729Z",
@@ -200,6 +207,12 @@ Example of response
"filename": "artifacts.zip",
"size": 1000
},
+ "artifacts": [
+ {"file_type": "archive", "size": 1000, "filename": "artifacts.zip", "file_format": "zip"},
+ {"file_type": "metadata", "size": 186, "filename": "metadata.gz", "file_format": "gzip"},
+ {"file_type": "trace", "size": 1500, "filename": "job.log", "file_format": "raw"},
+ {"file_type": "junit", "size": 750, "filename": "junit.xml.gz", "file_format": "gzip"}
+ ],
"finished_at": "2015-12-24T17:54:27.895Z",
"artifacts_expire_at": "2016-01-23T17:54:27.895Z",
"id": 7,
@@ -211,6 +224,7 @@ Example of response
"status": "pending"
},
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:27.722Z",
@@ -267,7 +281,6 @@ Example of response
},
"coverage": null,
"created_at": "2015-12-24T15:51:21.880Z",
- "artifacts_file": null,
"finished_at": "2015-12-24T17:54:31.198Z",
"artifacts_expire_at": "2016-01-23T17:54:31.198Z",
"id": 8,
@@ -279,6 +292,7 @@ Example of response
"status": "pending"
},
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": "2015-12-24T17:54:30.733Z",
@@ -458,11 +472,11 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
"finished_at": "2016-01-11T10:14:09.526Z",
"id": 42,
"name": "rubocop",
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": null,
@@ -505,11 +519,11 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
"finished_at": null,
"id": 42,
"name": "rubocop",
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": null,
@@ -559,6 +573,7 @@ Example of response
"id": 42,
"name": "rubocop",
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"created_at": "2016-01-11T10:13:33.506Z",
@@ -610,6 +625,7 @@ Example response:
"id": 42,
"name": "rubocop",
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"created_at": "2016-01-11T10:13:33.506Z",
@@ -654,11 +670,11 @@ Example of response
},
"coverage": null,
"created_at": "2016-01-11T10:13:33.506Z",
- "artifacts_file": null,
"finished_at": null,
"id": 42,
"name": "rubocop",
"ref": "master",
+ "artifacts": [],
"runner": null,
"stage": "test",
"started_at": null,
diff --git a/doc/api/settings.md b/doc/api/settings.md
index 68fc56b1fa3..b480d62e16a 100644
--- a/doc/api/settings.md
+++ b/doc/api/settings.md
@@ -56,7 +56,8 @@ Example response:
"enforce_terms": true,
"terms": "Hello world!",
"performance_bar_allowed_group_id": 42,
- "instance_statistics_visibility_private": false
+ "instance_statistics_visibility_private": false,
+ "user_show_add_ssh_key_message": true
}
```
@@ -161,6 +162,8 @@ PUT /application/settings
| `enforce_terms` | boolean | no | Enforce application ToS to all users |
| `terms` | text | yes (if `enforce_terms` is true) | Markdown content for the ToS |
| `instance_statistics_visibility_private` | boolean | no | When set to `true` Instance statistics will only be available to admins |
+| `user_show_add_ssh_key_message` | boolean | no | When set to `false` disable the "You won't be able to pull or push
++project code via SSH" warning shown to users with no uploaded SSH key |
```bash
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/application/settings?signup_enabled=false&default_project_visibility=internal
@@ -206,6 +209,7 @@ Example response:
"enforce_terms": true,
"terms": "Hello world!",
"performance_bar_allowed_group_id": 42,
- "instance_statistics_visibility_private": false
+ "instance_statistics_visibility_private": false,
+ "user_show_add_ssh_key_message": true
}
```
diff --git a/doc/api/system_hooks.md b/doc/api/system_hooks.md
index dd424470b67..7b8db6cfa8f 100644
--- a/doc/api/system_hooks.md
+++ b/doc/api/system_hooks.md
@@ -34,6 +34,7 @@ Example response:
"push_events":true,
"tag_push_events":false,
"merge_requests_events": true,
+ "repository_update_events": true,
"enable_ssl_verification":true
}
]
@@ -56,6 +57,7 @@ POST /hooks
| `push_events` | boolean | no | When true, the hook will fire on push events |
| `tag_push_events` | boolean | no | When true, the hook will fire on new tags being pushed |
| `merge_requests_events` | boolean | no | Trigger hook on merge requests events |
+| `repository_update_events` | boolean | no | Trigger hook on repository update events |
| `enable_ssl_verification` | boolean | no | Do SSL verification when triggering the hook |
Example request:
@@ -75,6 +77,7 @@ Example response:
"push_events":true,
"tag_push_events":false,
"merge_requests_events": true,
+ "repository_update_events": true,
"enable_ssl_verification":true
}
]
@@ -127,4 +130,4 @@ Example request:
```bash
curl --request DELETE --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" https://gitlab.example.com/api/v4/hooks/2
-```
+``` \ No newline at end of file
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/examples/README.md b/doc/ci/examples/README.md
index 811f4d1f07a..8eb96ae10b2 100644
--- a/doc/ci/examples/README.md
+++ b/doc/ci/examples/README.md
@@ -43,6 +43,10 @@ There's also a collection of repositories with [example projects](https://gitlab
- [Using `dpl` as deployment tool](deployment/README.md)
- [The `.gitlab-ci.yml` file for GitLab itself](https://gitlab.com/gitlab-org/gitlab-ce/blob/master/.gitlab-ci.yml)
+## Test Reports
+
+[Collect test reports in Verify stage](../junit_test_reports.md).
+
## Code Quality analysis
**(Starter)** [Analyze your project's Code Quality](code_quality.md).
diff --git a/doc/ci/img/junit_test_report.png b/doc/ci/img/junit_test_report.png
new file mode 100644
index 00000000000..ad098eb457f
--- /dev/null
+++ b/doc/ci/img/junit_test_report.png
Binary files differ
diff --git a/doc/ci/junit_test_reports.md b/doc/ci/junit_test_reports.md
new file mode 100644
index 00000000000..5ae8ecaafa6
--- /dev/null
+++ b/doc/ci/junit_test_reports.md
@@ -0,0 +1,102 @@
+# JUnit test reports
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/45318) in GitLab 11.2.
+Requires GitLab Runner 11.2 and above.
+
+## Overview
+
+It is very common that a [CI/CD pipeline](pipelines.md) contains a
+test job that will verify your code.
+If the tests fail, the pipeline fails and users get notified. The person that
+works on the merge request will have to check the job logs and see where the
+tests failed so that they can fix them.
+
+You can configure your job to use JUnit test reports, and GitLab will display a
+report on the merge request so that it's easier and faster to identify the
+failure without having to check the entire log.
+
+## Use cases
+
+Consider the following workflow:
+
+1. Your `master` branch is rock solid, your project is using GitLab CI/CD and
+ your pipelines indicate that there isn't anything broken.
+1. Someone from you team submits a merge request, a test fails and the pipeline
+ gets the known red icon. To investigate more, you have to go through the job
+ logs to figure out the cause of the failed test, which usually contain
+ thousands of lines.
+1. You configure the JUnit test reports and immediately GitLab collects and
+ exposes them in the merge request. No more searching in the job logs.
+1. Your development and debugging workflow becomes easier, faster and efficient.
+
+## How it works
+
+First, GitLab Runner uploads all JUnit XML files as artifacts to GitLab. Then,
+when you visit a merge request, GitLab starts comparing the head and base branch's
+JUnit test reports, where:
+
+- The base branch is the target branch (usually `master`).
+- The head branch is the source branch (the latest pipeline in each merge request).
+
+The reports panel has a summary showing how many tests failed and how many were fixed.
+If no comparison can be done because data for the base branch is not available,
+the panel will just show the list of failed tests for head.
+
+There are three types of results:
+
+1. **Newly failed tests:** Test cases which passed on base branch and failed on head branch
+1. **Existing failures:** Test cases which failed on base branch and failed on head branch
+1. **Resolved failures:** Test cases which failed on base branch and passed on head branch
+
+Each entry in the panel will show the test name and its type from the list
+above. Clicking on the test name will open a modal window with details of its
+execution time and the error output.
+
+![Test Reports Widget](img/junit_test_report.png)
+
+## How to set it up
+
+NOTE: **Note:**
+For a list of supported languages on JUnit tests, check the
+[Wikipedia article](https://en.wikipedia.org/wiki/JUnit#Ports).
+
+To enable the JUnit reports in merge requests, you need to add
+[`artifacts:reports:junit`](yaml/README.md#artifacts-reports-junit)
+in `.gitlab-ci.yml`, and specify the path(s) of the generated test reports.
+
+In the following examples, the job in the `test` stage runs and GitLab
+collects the JUnit test report from each job. After each job is executed, the
+XML reports are stored in GitLab as artifacts and their results are shown in the
+merge request widget.
+
+### Ruby example
+
+Use the following job in `.gitlab-ci.yml`:
+
+```yaml
+## Use https://github.com/sj26/rspec_junit_formatter to generate a JUnit report with rspec
+ruby:
+ stage: test
+ script:
+ - bundle install
+ - rspec spec/lib/ --format RspecJunitFormatter --out rspec.xml
+ artifacts:
+ reports:
+ junit: rspec.xml
+```
+
+### Go example
+
+Use the following job in `.gitlab-ci.yml`:
+
+```yaml
+## Use https://github.com/jstemmer/go-junit-report to generate a JUnit report with go
+golang:
+ stage: test
+ script:
+ - go get -u github.com/jstemmer/go-junit-report
+ - go test -v 2>&1 | go-junit-report > report.xml
+ artifacts:
+ reports:
+ junit: report.xml
+```
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index 95d705d3a3d..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'
@@ -1092,6 +1094,52 @@ job:
expire_in: 1 week
```
+### `artifacts:reports`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in
+GitLab 11.2. Requires GitLab Runner 11.2 and above.
+
+The `reports` keyword is used for collecting test reports from jobs and
+exposing them in GitLab's UI (merge requests, pipeline views). Read how to use
+this with [JUnit reports](#artifacts-reports-junit).
+
+NOTE: **Note:**
+The test reports are collected regardless of the job results (success or failure).
+You can use [`artifacts:expire_in`](#artifacts-expire_in) to set up an expiration
+date for their artifacts.
+
+#### `artifacts:reports:junit`
+
+> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/20390) in
+GitLab 11.2. Requires GitLab Runner 11.2 and above.
+
+The `junit` report collects [JUnit XML files](https://www.ibm.com/support/knowledgecenter/en/SSQ2R2_14.1.0/com.ibm.rsar.analysis.codereview.cobol.doc/topics/cac_useresults_junit.html)
+as artifacts. Although JUnit was originally developed in Java, there are many
+[third party ports](https://en.wikipedia.org/wiki/JUnit#Ports) for other
+languages like Javascript, Python, Ruby, etc.
+
+Below is an example of collecting a JUnit XML file from Ruby's RSpec test tool:
+
+```yaml
+rspec:
+ stage: test
+ script:
+ - bundle install
+ - rspec --format RspecJunitFormatter --out rspec.xml
+ artifacts:
+ reports:
+ junit: rspec.xml
+```
+
+The collected JUnit reports will be uploaded to GitLab as an artifact and will
+be automatically [shown in merge requests](../junit_test_reports.md).
+
+NOTE: **Note:**
+In case the JUnit tool you use exports to multiple XML files, you can specify
+multiple test report paths within a single job
+(`junit: [rspec-1.xml, rspec-2.xml, rspec-3.xml]`) and they will be automatically
+concatenated into a single file.
+
## `dependencies`
> Introduced in GitLab 8.6 and GitLab Runner v1.1.1.
diff --git a/doc/development/documentation/index.md b/doc/development/documentation/index.md
index f5cdd310f6f..f46c171d9f1 100644
--- a/doc/development/documentation/index.md
+++ b/doc/development/documentation/index.md
@@ -25,52 +25,23 @@ them to review it for you.
We use the [monthly release blog post](https://about.gitlab.com/handbook/marketing/blog/release-posts/#monthly-releases) as a changelog checklist to ensure everything
is documented.
-Whenever you submit a merge request for the documentation, use the documentation MR description template.
+Whenever you submit a merge request for the documentation, use the
+"Documentation" MR description template. If you're changing documentation
+location, use the MR description template called "Change documentation
+location" instead.
-Please check the [documentation workflow](https://about.gitlab.com/handbook/product/technical-writing/workflow/) before getting started.
+## Documentation workflow
-## Documentation structure
-
-- Overview and use cases: what it is, why it is necessary, why one would use it
-- Requirements: what do we need to get started
-- Tutorial: how to set it up, how to use it
-
-Always link a new document from its topic-related index, otherwise, it will
-not be included it in the documentation site search.
-
-_Note: to be extended._
-
-### Feature overview and use cases
-
-Every major feature (regardless if present in GitLab Community or Enterprise editions)
-should present, at the beginning of the document, two main sections: **overview** and
-**use cases**. Every GitLab EE-only feature should also contain these sections.
+Please read through the [documentation workflow](workflow.md) before getting started.
-**Overview**: as the name suggests, the goal here is to provide an overview of the feature.
-Describe what is it, what it does, why it is important/cool/nice-to-have,
-what problem it solves, and what you can do with this feature that you couldn't
-do before.
-
-**Use cases**: provide at least two, ideally three, use cases for every major feature.
-You should answer this question: what can you do with this feature/change? Use cases
-are examples of how this feature or change can be used in real life.
-
-Examples:
-- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
-- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
-- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
-- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
-
-Note that if you don't have anything to add between the doc title (`<h1>`) and
-the header `## Overview`, you can omit the header, but keep the content of the
-overview there.
+## Documentation structure
-> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
-and for every **major** feature present in Community Edition.
+Follow through the [documentation structure guide](structure.md) for learning
+how to structure GitLab docs.
## Markdown and styles
-Currently GitLab docs use Redcarpet as [markdown](../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
+Currently GitLab docs use Redcarpet as [markdown](../../user/markdown.md) engine, but there's an [open discussion](https://gitlab.com/gitlab-com/gitlab-docs/issues/50) for implementing Kramdown in the near future.
All the docs follow the [documentation style guidelines](styleguide.md).
@@ -84,9 +55,18 @@ In order to have a [solid site structure](https://searchengineland.com/seo-benef
all docs should be linked. Every new document should be cross-linked to its related documentation, and linked from its topic-related index, when existent.
The directories `/workflow/`, `/gitlab-basics/`, `/university/`, and `/articles/` have
-been deprecated and the majority their docs have been moved to their correct location
+been **deprecated** and the majority their docs have been moved to their correct location
in small iterations. Please don't create new docs in these folders.
+### Documentation files
+
+- When you create a new directory, always start with an `index.md` file.
+Do not use another file name and **do not** create `README.md` files
+- **Do not** use special chars and spaces, or capital letters in file names,
+directory names, branch names, and anything that generates a path.
+- Max screenshot size: 100KB
+- We do not support videos (yet)
+
### Location and naming documents
The documentation hierarchy can be vastly improved by providing a better layout
@@ -116,7 +96,7 @@ The table below shows what kind of documentation goes where.
---
-**General rules:**
+**General rules & best practices:**
1. The correct naming and location of a new document, is a combination
of the relative URL of the document in question and the GitLab Map design
@@ -203,7 +183,7 @@ Things to note:
documentation, sometimes it might be useful to search a path deeper.
- The `*.md` extension is not used when a document is linked to GitLab's
built-in help page, that's why we omit it in `git grep`.
-- Use the checklist on the documentation MR description template.
+- Use the checklist on the "Change documentation location" MR description template.
#### Alternative redirection method
@@ -514,7 +494,7 @@ Suppose there's a process to go from point A to point B in 5 steps: `(A) 1 > 2 >
A **guide** can be understood as a description of certain processes to achieve a particular objective. A guide brings you from A to B describing the characteristics of that process, but not necessarily going over each step. It can mention, for example, steps 2 and 3, but does not necessarily explain how to accomplish them.
-- Live example: "[Static sites and GitLab Pages domains (Part 1)](../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)"
+- Live example: "[Static sites and GitLab Pages domains (Part 1)](../../user/project/pages/getting_started_part_one.md) to [Creating and Tweaking GitLab CI/CD for GitLab Pages (Part 4)](../../user/project/pages/getting_started_part_four.md)"
A **tutorial** requires a clear **step-by-step** guidance to achieve a singular objective. It brings you from A to B, describing precisely all the necessary steps involved in that process, showing each of the 5 steps to go from A to B.
It does not only describes steps 2 and 3, but also shows you how to accomplish them.
diff --git a/doc/development/documentation/structure.md b/doc/development/documentation/structure.md
new file mode 100644
index 00000000000..1002836096a
--- /dev/null
+++ b/doc/development/documentation/structure.md
@@ -0,0 +1,149 @@
+---
+description: Learn the how to correctly structure GitLab documentation.
+---
+
+# Documentation structure
+
+For consistency throughout the documentation, it's important to maintain the same
+structure among the docs.
+
+Before getting started, read through the following docs:
+
+- [Contributing to GitLab documentation](index.md#contributing-to-docs)
+- [Merge requests for GitLab documentation](index.md#merge-requests-for-gitlab-documentation)
+- [Branch naming for docs-only changes](index.md#branch-naming)
+- [Documentation directory structure](index.md#documentation-directory-structure)
+- [Documentation style guidelines](styleguide.md)
+- [Documentation workflow](workflow.md)
+
+## Documentation blurb
+
+Every document should include the following content in the following sequence:
+
+- **Feature name**: defines an intuitive name for the feature that clearly
+states what it is and is consistent with any relevant UI text.
+- **Feature overview** and description: describe what it is, what it does, and in what context it should be used.
+- **Use cases**: describes real use case scenarios for that feature.
+- **Requirements**: describes what software and/or configuration is required to be able to
+use the feature and, if applicable, prerequisite knowledge for being able to follow/implement the tutorial.
+For example, familiarity with GitLab CI/CD, an account on a third-party service, dependencies installed, etc.
+Link each one to its most relevant resource; i.e., where the reader can go to begin to fullfil that requirement.
+(Another doc page, a third party application's site, etc.)
+- **Instructions**: clearly describes the steps to use the feature, leaving no gaps.
+- **Troubleshooting** guide (recommended but not required): if you know beforehand what issues
+one might have when setting it up, or when something is changed, or on upgrading, it's
+important to describe those too. Think of things that may go wrong and include them in the
+docs. This is important to minimize requests for support, and to avoid doc comments with
+questions that you know someone might ask. Answering them beforehand only makes your
+document better and more approachable.
+
+For additional details, see the subsections below, as well as the [Documentation template for new docs](#Documentation-template-for-new-docs).
+
+### Feature overview and use cases
+
+Every major feature (regardless if present in GitLab Community or Enterprise editions)
+should present, at the beginning of the document, two main sections: **overview** and
+**use cases**. Every GitLab EE-only feature should also contain these sections.
+
+**Overview**: as the name suggests, the goal here is to provide an overview of the feature.
+Describe what is it, what it does, why it is important/cool/nice-to-have,
+what problem it solves, and what you can do with this feature that you couldn't
+do before.
+
+**Use cases**: provide at least two, ideally three, use cases for every major feature.
+You should answer this question: what can you do with this feature/change? Use cases
+are examples of how this feature or change can be used in real life.
+
+Examples:
+- CE and EE: [Issues](../user/project/issues/index.md#use-cases)
+- CE and EE: [Merge Requests](../user/project/merge_requests/index.md#overview)
+- EE-only: [Geo](https://docs.gitlab.com/ee/gitlab-geo/README.html#overview)
+- EE-only: [Jenkins integration](https://docs.gitlab.com/ee/integration/jenkins.md#overview)
+
+Note that if you don't have anything to add between the doc title (`<h1>`) and
+the header `## Overview`, you can omit the header, but keep the content of the
+overview there.
+
+> **Overview** and **use cases** are required to **every** Enterprise Edition feature,
+and for every **major** feature present in Community Edition.
+
+### Discoverability
+
+Your new document will be discoverable by the user only if:
+
+- Crosslinked from the higher-level index (e.g., Issue Boards docs
+should be linked from Issues; Prometheus docs should be linked from
+Monitoring; CI/CD tutorials should be linked from CI/CD examples).
+ - When referencing other GitLab products and features, link to their
+respective docs; when referencing third-party products or technologies,
+link out to their external sites, documentation, and resources.
+- The headings are clear. E.g., "App testing" is a bad heading, "Testing
+an application with GitLab CI/CD" is much better. Think of something
+someone will search for and use these keywords in the headings.
+
+## Documentation template for new docs
+
+To start a new document, respect the file tree and file name guidelines,
+as well as the style guidelines. Use the following template:
+
+```md
+---
+description: "short document description." # Up to ~200 chars long. They will be displayed in Google Search Snippets.
+---
+
+# Feature Name **[TIER]** (1)
+
+> [Introduced](link_to_issue_or_mr) in GitLab Tier X.Y (2).
+
+A short description for the feature (can be the same used in the frontmatter's
+`description`).
+
+## Overview
+
+To write the feature overview, you should consider answering the following questions:
+
+- What is it?
+- Who is it for?
+- What is the context in which it is used and are there any prerequisites/requirements?
+- What can the user do with it? (Be sure to consider multiple audiences, like GitLab admin and developer-user.)
+- What are the benefits to using it over any alternatives?
+
+## Use cases
+
+Describe one to three use cases for that feature. Give real-life examples.
+
+## Requirements
+
+State any requirements, if any, for using the feature and/or following along with the tutorial.
+
+The only assumption that is redundant and doesn't need to be mentioned is having an account
+on GitLab.
+
+## Instructions
+
+("Instructions" is not necessarily the name of the heading)
+
+- Write a step-by-step guide, with no gaps between the steps.
+- Start with an h2 (`##`), break complex steps into small steps using
+subheadings h3 > h4 > h5 > h6. _Never skip the hierarchy level, such
+as h2 > h4_, as it will break the TOC and may affect the breadcrumbs.
+- Use short and descriptive headings (up to ~50 chars). You can use one
+single heading `## How it works` for the instructions when the feature
+is simple and the document is short.
+- Be clear, concise, and stick to the goal of the doc: explain how to
+use that feature.
+- Use inclusive language and avoid jargons, as well as uncommon and
+fancy words. The docs should be clear and very easy to understand.
+- Write in the 3rd person (use "we", "you", "us", "one", instead of "I" or "me").
+- Always provide internal and external reference links.
+- Always link the doc from its higher-level index.
+
+<!-- ## Troubleshooting
+
+Add a troubleshooting guide when possible/applicable. -->
+```
+
+Notes:
+
+- (1): Apply the [tier badges](styleguide.md#product-badges) accordingly
+- (2): Apply the correct format for the [GitLab version introducing the feature](styleguide.md#gitlab-versions-and-tiers)
diff --git a/doc/development/documentation/styleguide.md b/doc/development/documentation/styleguide.md
index ad49c77aac8..6c60a517b6d 100644
--- a/doc/development/documentation/styleguide.md
+++ b/doc/development/documentation/styleguide.md
@@ -10,6 +10,22 @@ GitLab documentation. Check the
Check the GitLab handbook for the [writing styles guidelines](https://about.gitlab.com/handbook/communication/#writing-style-guidelines).
+## Files
+
+- [Directory structure](index.md#location-and-naming-documents): place the docs
+in the correct location
+- [Documentation files](index.md#documentation-files): name the files accordingly
+- [Markdown](../../user/markdown.md): use the GitLab Flavored Markdown in the
+documentation
+
+NOTE: **Note:**
+**Do not** use capital letters, spaces, or special chars in file names,
+branch names, directory names, headings, or in anything that generates a path.
+
+NOTE: **Note:**
+**Do not** create new `README.md` files, name them `index.md` instead. There's
+a test that will fail if it spots a new `README.md` file.
+
## Text
- Split up long lines (wrap text), this makes it much easier to review and edit. Only
@@ -61,7 +77,8 @@ For punctuation rules, please refer to the [GitLab UX guide](https://design.gitl
- Add **only one H1** in each document, by adding `#` at the beginning of
it (when using markdown). The `h1` will be the document `<title>`.
-- For subheadings, use `##`, `###` and so on
+- Start with an h2 (`##`), and respect the order h2 > h3 > h4 > h5 > h6.
+ Never skip the hierarchy level, such as h2 > h4
- Avoid putting numbers in headings. Numbers shift, hence documentation anchor
links shift too, which eventually leads to dead links. If you think it is
compelling to add numbers in headings, make sure to at least discuss it with
@@ -115,10 +132,7 @@ needs to expand the tab to find the settings you're referring to
the `.md` document that you're working on is located. Always prepend their
names with the name of the document that they will be included in. For
example, if there is a document called `twitter.md`, then a valid image name
- could be `twitter_login_screen.png`. [**Exception**: images for
- [articles](index.md#technical-articles) should be
- put in a directory called `img` underneath `/articles/article_title/img/`, therefore,
- there's no need to prepend the document name to their filenames.]
+ could be `twitter_login_screen.png`.
- Images should have a specific, non-generic name that will differentiate them.
- Keep all file names in lower case.
- Consider using PNG images instead of JPEG.
@@ -126,6 +140,8 @@ needs to expand the tab to find the settings you're referring to
- Compress gifs with <https://ezgif.com/optimize> or similar tool.
- Images should be used (only when necessary) to _illustrate_ the description
of a process, not to _replace_ it.
+- Max image size: 100KB (gifs included).
+- The GitLab docs do not support videos yet.
Inside the document:
diff --git a/doc/development/documentation/workflow.md b/doc/development/documentation/workflow.md
new file mode 100644
index 00000000000..339ec80f889
--- /dev/null
+++ b/doc/development/documentation/workflow.md
@@ -0,0 +1,186 @@
+---
+description: Learn the process of shipping documentation for GitLab.
+---
+
+# Documentation process at GitLab
+
+At GitLab, developers contribute new or updated documentation along with their code, but product managers and technical writers also have essential roles in the process.
+
+- Product Managers (PMs): in the issue for all new and updated features,
+PMs include specific documentation requirements that the developer who is
+writing or updating the docs must meet, along with feature descriptions
+and use cases. They call out any specific areas where collaborating with
+a technical writer is recommended, and usually act as the first reviewer
+of the docs.
+- Developers: author documentation and merge it on time (up to a week after
+the feature freeze).
+- Technical Writers: review each issue to ensure PM's requirements are complete,
+help developers with any questions throughout the process, and act as the final
+reviewer of all new and updated docs content before it's merged.
+
+## Requirements
+
+Documentation must be delivered whenever:
+
+- A new feature is shipped
+- There are changes to the UI
+- A process, workflow, or previously documented feature is changed
+
+Documentation is not required when a feature is changed on the backend
+only and does not directly affect the way that any regular user or
+administrator would interact with GitLab.
+
+NOTE: **Note:**
+When refactoring documentation in needed, it should be submitted it in its own MR.
+**Do not** join new features' MRs with refactoring existing docs, as they might have
+different priorities.
+
+NOTE: **Note:**
+[Smaller MRs are better](https://gitlab.com/gitlab-com/blog-posts/issues/185#note_4401010)! Do not mix subjects, and ship the smallest MR possible.
+
+### Documentation review process
+
+The docs shipped by the developer should be reviewed by the PM (for accuracy) and a Technical Writer (for clarity and structure).
+
+#### Documentation updates that require Technical Writer review
+
+Every documentation change that meets the criteria below must be reviewed by a Technical Writer
+to ensure clarity and discoverability, and avoid redundancy, bad file locations, typos, broken links, etc.
+Within the GitLab issue or MR, ping the relevant technical writer for the subject area. If you're not sure who that is,
+ping any of them or all of them (`@gl\-docsteam`).
+
+A Technical Writer must review documentation updates that involve:
+
+- Docs introducing new features
+- Changing documentation location
+- Refactoring existing documentation
+- Creating new documentation files
+
+If you need any help to choose the correct place for a doc, discuss a documentation
+idea or outline, or request any other help, ping a Technical Writer on your issue, MR,
+or on Slack in `#docs`.
+
+#### Skip the PM's review
+
+When there's a non-significant change to the docs, you can skip the review
+of the PM. Add the same labels as you would for a regular doc change and
+assign the correct milestone. In these cases, assign a Technical Writer
+for approval/merge, or mention `@gl\-docsteam` in case you don't know
+which Tech Writer to assign for.
+
+#### Skip the entire review
+
+When the MR only contains corrections to the content (typos, grammar,
+broken links, etc), it can be merged without the PM's and Tech Writer's review.
+
+## Documentation structure
+
+Read through the [documentation structure](structure.md) docs for an overview.
+
+## Documentation workflow
+
+To follow a consistent workflow every month, documentation changes
+involve the Product Managers, the developer who shipped the feature,
+and the Technical Writing team. Each role is described below.
+
+### 1. Product Manager's role in the documentation process
+
+The Product Manager (PM) should add to the feature issue:
+
+- Feature name, overview/description, and use cases, for the [documentation blurb](structure.md#documentation-blurb)
+- The documentation requirements for the developer working on the docs
+ - What new page, new subsection of an existing page, or other update to an existing page/subsection is needed.
+ - Just one page/section/update or multiple (perhaps there's an end user and admin change needing docs, or we need to update a previously recommended workflow, or we want to link the new feature from various places; consider and mention all ways documentation should be affected
+ - Suggested title of any page or subsection, if applicable
+- Label the issue with `Documentation`, `Deliverable`, `docs:P1`, and assign
+ the correct milestone
+
+### 2. Developer's role in the documentation process
+
+As a developer, or as a community contributor, you should ship the documentation
+with the feature, as in GitLab the documentation is part of the product.
+
+The docs can either be shipped along with the MR introducing the code, or,
+alternatively, created from a follow-up issue and MR.
+
+The docs should be shipped **by the feature freeze date**. Justified
+exceptions are accepted, as long as the [following process](#documentation-shipped-late)
+and the missed-deliverable due date (the 14th of each month) are both respected.
+
+#### Documentation shipped in the feature MR
+
+The developer should add to the feature MR the documentation containing:
+
+- The [documentation blurb](structure.md#documentation-blurb): copy the
+feature name, overview/description, and use cases from the feature issue
+- Instructions: write how to use the feature, step by step, with no gaps.
+- [Crosslink for discoverability](structure.md#discoverability): link with
+internal docs and external resources (if applicable)
+- Index: link the new doc or the new heading from the higher-level index
+for [discoverability](#discoverability)
+- [Screenshots](styleguide.md#images): when necessary, add screenshots for:
+ - Illustrating a step of the process
+ - Indicating the location of a navigation menu
+- Label the MR with `Documentation`, `Deliverable`, `docs-P1`, and assign
+the correct milestone
+- Assign the PM for review
+- When done, mention the `@gl\-docsteam` in the MR asking for review
+- **Due date**: feature freeze date and time
+
+#### Documentation shipped in a follow-up MR
+
+If the docs aren't being shipped within the feature MR:
+
+- Create a new issue mentioning "docs" or "documentation" in the title (use the Documentation issue description template)
+- Label the issue with: `Documentation`, `Deliverable`, `docs-P1`, `<product-label>`
+(product label == CI/CD, Pages, Prometheus, etc)
+- Add the correct milestone
+- Create a new MR for shipping the docs changes and follow the same
+process [described above](#documentation-shipped-in-the-feature-mr)
+- Use the MR description template called "Documentation"
+- Add the same labels and milestone as you did for the issue
+- Assign the PM for review
+- When done, mention the `@gl\-docsteam` in the MR asking for review
+- **Due date**: feature freeze date and time
+
+#### Documentation shipped late
+
+Shipping late means that you are affecting the whole feature workflow
+as well as other teams' priorities (PMs, tech writers, release managers,
+release post reviewers), so every effort should be made to avoid this.
+
+If you did not ship the docs within the feature freeze, proceed as
+[described above](#documentation-shipped-in-a-follow-up-mr) and,
+besides the regular labels, include the labels `Pick into X.Y` and
+`missed-deliverable` in the issue and the MR, and assign them the correct
+milestone.
+
+The **due date** for **merging** `missed-deliverable` MRs is on the
+**14th** of each month.
+
+### 3. Technical Writer's role in the documentation process
+
+- **Planning**
+ - Once an issue contains a Documentation label and the current milestone, a
+technical writer reviews the Product Manager's documentation requirements
+ - Once the documentation requirements are approved, the technical writer can
+work with the developer to discuss any documentation questions and plans/outlines, as needed.
+
+- **Review** - A technical writer must review the documentation for:
+ - Clarity
+ - Relevance (make sure the content is appropriate given the impact of the feature)
+ - Location (make sure the doc is in the correct dir and has the correct name)
+ - Syntax, typos, and broken links
+ - Improvements to the content
+ - Accordance to the [docs style guide](styleguide.md)
+
+<!-- TBA: issue and MR description templates as part of the process -->
+
+<!--
+## New features vs feature updates
+
+- TBA:
+ - Describe the difference between new features and feature updates
+ - Creating a new doc vs updating an existing doc
+-->
+
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/feature_flags.md b/doc/development/feature_flags.md
index 5d1f657015c..09ea8c05be6 100644
--- a/doc/development/feature_flags.md
+++ b/doc/development/feature_flags.md
@@ -20,7 +20,40 @@ dynamic (querying the DB etc.).
Once defined in `lib/feature.rb`, you will be able to activate a
feature for a given feature group via the [`feature_group` param of the features API](../api/features.md#set-or-create-a-feature)
+For GitLab.com, team members have access to feature flags through chatops. Only
+percentage gates are supported at this time. Setting a feature to be used 50% of
+the time, you should execute `/chatops run feature set my_feature_flag 50`.
+
## Feature flags for user applications
GitLab does not yet support the use of feature flags in deployed user applications.
-You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779). \ No newline at end of file
+You can follow the progress on that [in the issue on our issue tracker](https://gitlab.com/gitlab-org/gitlab-ee/issues/779).
+
+## Developing with feature flags
+
+In general, it's better to have a group- or user-based gate, and you should prefer
+it over the use of percentage gates. This would make debugging easier, as you
+filter for example logs and errors based on actors too. Futhermore, this allows
+for enabling for the `gitlab-org` group first, while the rest of the users
+aren't impacted.
+
+```ruby
+# Good
+Feature.enabled?(:feature_flag, project)
+
+# Avoid, if possible
+Feature.enabled?(:feature_flag)
+```
+
+To use feature gates based on actors, the model needs to respond to
+`flipper_id`. For example, to enable for the Foo model:
+
+```ruby
+class Foo < ActiveRecord::Base
+ include FeatureGate
+end
+```
+
+Features that are developed and are intended to be merged behind a feature flag
+should not include a changelog entry. The entry should be added in the merge
+request removing the feature flags.
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/merge_requests/index.md b/doc/user/project/merge_requests/index.md
index 86ecf33ed31..43ca498d006 100644
--- a/doc/user/project/merge_requests/index.md
+++ b/doc/user/project/merge_requests/index.md
@@ -43,8 +43,7 @@ A. Consider you are a software developer working in a team:
1. You checkout a new branch, and submit your changes through a merge request
1. You gather feedback from your team
-1. You work on the implementation optimizing code with [Code Quality reports](https://docs.gitlab.com/ee/user/project/merge_requests/code_quality.html) **[STARTER]**
-1. You build and test your changes with GitLab CI/CD
+1. You verify your changes with [JUnit test reports](../../../ci/junit_test_reports.md) in GitLab CI/CD
1. You request the approval from your manager
1. Your manager pushes a commit with his final review, [approves the merge request](https://docs.gitlab.com/ee/user/project/merge_requests/merge_request_approvals.html), and set it to [merge when pipeline succeeds](#merge-when-pipeline-succeeds) (Merge Request Approvals are available in GitLab Starter)
1. Your changes get deployed to production with [manual actions](../../../ci/yaml/README.md#manual-actions) for GitLab CI/CD
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/api/entities.rb b/lib/api/entities.rb
index 453ebb9c669..b6393fdef19 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -1080,6 +1080,10 @@ module API
expose :filename, :size
end
+ class JobArtifact < Grape::Entity
+ expose :file_type, :size, :filename, :file_format
+ end
+
class JobBasic < Grape::Entity
expose :id, :status, :stage, :name, :ref, :tag, :coverage
expose :created_at, :started_at, :finished_at
@@ -1094,7 +1098,9 @@ module API
end
class Job < JobBasic
+ # artifacts_file is included in job_artifacts, but kept for backward compatibility (remove in api/v5)
expose :artifacts_file, using: JobArtifactFile, if: -> (job, opts) { job.artifacts? }
+ expose :job_artifacts, as: :artifacts, using: JobArtifact
expose :runner, with: Runner
expose :artifacts_expire_at
end
@@ -1153,7 +1159,7 @@ module API
class License < Grape::Entity
expose :key, :name, :nickname
- expose :featured, as: :popular
+ expose :popular?, as: :popular
expose :url, as: :html_url
expose(:source_url) { |license| license.meta['source'] }
expose(:description) { |license| license.meta['description'] }
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 10c6e565f09..fc8c52085ab 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -38,7 +38,7 @@ module API
builds = user_project.builds.order('id DESC')
builds = filter_builds(builds, params[:scope])
- builds = builds.preload(:user, :job_artifacts_archive, :runner, pipeline: :project)
+ builds = builds.preload(:user, :job_artifacts_archive, :job_artifacts, :runner, pipeline: :project)
present paginate(builds), with: Entities::Job
end
@@ -54,7 +54,7 @@ module API
pipeline = user_project.pipelines.find(params[:pipeline_id])
builds = pipeline.builds
builds = filter_builds(builds, params[:scope])
- builds = builds.preload(:job_artifacts_archive, project: [:namespace])
+ builds = builds.preload(:job_artifacts_archive, :job_artifacts, project: [:namespace])
present paginate(builds), with: Entities::Job
end
diff --git a/lib/api/templates.rb b/lib/api/templates.rb
index 41862768a3f..927baaea652 100644
--- a/lib/api/templates.rb
+++ b/lib/api/templates.rb
@@ -16,31 +16,8 @@ module API
gitlab_version: 8.15
}
}.freeze
- PROJECT_TEMPLATE_REGEX =
- %r{[\<\{\[]
- (project|description|
- one\sline\s.+\swhat\sit\sdoes\.) # matching the start and end is enough here
- [\>\}\]]}xi.freeze
- YEAR_TEMPLATE_REGEX = /[<{\[](year|yyyy)[>}\]]/i.freeze
- FULLNAME_TEMPLATE_REGEX =
- %r{[\<\{\[]
- (fullname|name\sof\s(author|copyright\sowner))
- [\>\}\]]}xi.freeze
helpers do
- def parsed_license_template
- # We create a fresh Licensee::License object since we'll modify its
- # content in place below.
- template = Licensee::License.new(params[:name])
-
- template.content.gsub!(YEAR_TEMPLATE_REGEX, Time.now.year.to_s)
- template.content.gsub!(PROJECT_TEMPLATE_REGEX, params[:project]) if params[:project].present?
-
- fullname = params[:fullname].presence || current_user.try(:name)
- template.content.gsub!(FULLNAME_TEMPLATE_REGEX, fullname) if fullname
- template
- end
-
def render_response(template_type, template)
not_found!(template_type.to_s.singularize) unless template
present template, with: Entities::Template
@@ -56,11 +33,12 @@ module API
use :pagination
end
get "templates/licenses" do
- options = {
- featured: declared(params)[:popular].present? ? true : nil
- }
- licences = ::Kaminari.paginate_array(Licensee::License.all(options))
- present paginate(licences), with: Entities::License
+ popular = declared(params)[:popular]
+ popular = to_boolean(popular) if popular.present?
+
+ templates = LicenseTemplateFinder.new(popular: popular).execute
+
+ present paginate(::Kaminari.paginate_array(templates)), with: ::API::Entities::License
end
desc 'Get the text for a specific license' do
@@ -71,9 +49,15 @@ module API
requires :name, type: String, desc: 'The name of the template'
end
get "templates/licenses/:name", requirements: { name: /[\w\.-]+/ } do
- not_found!('License') unless Licensee::License.find(declared(params)[:name])
+ templates = LicenseTemplateFinder.new.execute
+ template = templates.find { |template| template.key == params[:name] }
+
+ not_found!('License') unless template.present?
- template = parsed_license_template
+ template.resolve!(
+ project_name: params[:project].presence,
+ fullname: params[:fullname].presence || current_user&.name
+ )
present template, with: ::API::Entities::License
end
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/git/repository_mirroring.rb b/lib/gitlab/git/repository_mirroring.rb
index 65eb5cc18cf..752a91fbb60 100644
--- a/lib/gitlab/git/repository_mirroring.rb
+++ b/lib/gitlab/git/repository_mirroring.rb
@@ -2,34 +2,7 @@ module Gitlab
module Git
module RepositoryMirroring
def remote_branches(remote_name)
- gitaly_migrate(:ref_find_all_remote_branches) do |is_enabled|
- if is_enabled
- gitaly_ref_client.remote_branches(remote_name)
- else
- Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- rugged_remote_branches(remote_name)
- end
- end
- end
- end
-
- private
-
- def rugged_remote_branches(remote_name)
- branches = []
-
- rugged.references.each("refs/remotes/#{remote_name}/*").map do |ref|
- name = ref.name.sub(%r{\Arefs/remotes/#{remote_name}/}, '')
-
- begin
- target_commit = Gitlab::Git::Commit.find(self, ref.target.oid)
- branches << Gitlab::Git::Branch.new(self, name, ref.target, target_commit)
- rescue Rugged::ReferenceError
- # Omit invalid branch
- end
- end
-
- branches
+ gitaly_ref_client.remote_branches(remote_name)
end
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/i18n.rb b/lib/gitlab/i18n.rb
index 343487bc361..b8213929c6a 100644
--- a/lib/gitlab/i18n.rb
+++ b/lib/gitlab/i18n.rb
@@ -22,7 +22,8 @@ module Gitlab
'tr_TR' => 'Türkçe',
'id_ID' => 'Bahasa Indonesia',
'fil_PH' => 'Filipino',
- 'pl_PL' => 'Polski'
+ 'pl_PL' => 'Polski',
+ 'cs_CZ' => 'Čeština'
}.freeze
def available_locales
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/gitlab/template/finders/base_template_finder.rb b/lib/gitlab/template/finders/base_template_finder.rb
index 473b05257c6..a5105439b12 100644
--- a/lib/gitlab/template/finders/base_template_finder.rb
+++ b/lib/gitlab/template/finders/base_template_finder.rb
@@ -21,7 +21,7 @@ module Gitlab
def category_directory(category)
return @base_dir unless category.present?
- @base_dir + @categories[category]
+ File.join(@base_dir, @categories[category])
end
class << self
diff --git a/lib/gitlab/template/finders/repo_template_finder.rb b/lib/gitlab/template/finders/repo_template_finder.rb
index 33f07fa0120..29bc2393ff9 100644
--- a/lib/gitlab/template/finders/repo_template_finder.rb
+++ b/lib/gitlab/template/finders/repo_template_finder.rb
@@ -27,7 +27,7 @@ module Gitlab
directory = select_directory(file_name)
raise FileNotFoundError if directory.nil?
- category_directory(directory) + file_name
+ File.join(category_directory(directory), file_name)
end
def list_files_for(dir)
@@ -37,8 +37,8 @@ module Gitlab
entries = @repository.tree(:head, dir).entries
- names = entries.map(&:name)
- names.select { |f| f =~ self.class.filter_regex(@extension) }
+ paths = entries.map(&:path)
+ paths.select { |f| f =~ self.class.filter_regex(@extension) }
end
private
@@ -47,10 +47,10 @@ module Gitlab
return [] unless @commit
# Insert root as directory
- directories = ["", @categories.keys]
+ directories = ["", *@categories.keys]
directories.find do |category|
- path = category_directory(category) + file_name
+ path = File.join(category_directory(category), file_name)
@repository.blob_at(@commit.id, path)
end
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 8f3914c5f69..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 ""
@@ -2971,6 +3037,9 @@ msgstr ""
msgid "IDE|Review"
msgstr ""
+msgid "IP Address"
+msgstr ""
+
msgid "Identifier"
msgstr ""
@@ -3061,6 +3130,9 @@ msgstr ""
msgid "Incompatible Project"
msgstr ""
+msgid "Indicates whether this runner can pick jobs without tags"
+msgstr ""
+
msgid "Inline"
msgstr ""
@@ -3133,12 +3205,51 @@ 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 ""
+
msgid "Job|Job has been erased"
msgstr ""
msgid "Job|Job has been erased by"
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 ""
@@ -3220,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] ""
@@ -3279,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 ""
@@ -3315,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 ""
@@ -3390,6 +3512,9 @@ msgstr ""
msgid "Maximum git storage failures"
msgstr ""
+msgid "Maximum job timeout"
+msgstr ""
+
msgid "May"
msgstr ""
@@ -3438,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 ""
@@ -3486,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 ""
@@ -3504,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 ""
@@ -3674,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 ""
@@ -3719,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 ""
@@ -3830,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 ""
@@ -3932,9 +4083,15 @@ msgstr ""
msgid "Pause"
msgstr ""
+msgid "Paused Runners don't accept new jobs"
+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 ""
@@ -3953,6 +4110,9 @@ msgstr ""
msgid "Pipeline"
msgstr ""
+msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
+msgstr ""
+
msgid "Pipeline Health"
msgstr ""
@@ -4037,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 ""
@@ -4058,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 ""
@@ -4172,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 ""
@@ -4346,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 ""
@@ -4424,6 +4599,9 @@ msgstr ""
msgid "Promote to group label"
msgstr ""
+msgid "Protected"
+msgstr ""
+
msgid "Protip:"
msgstr ""
@@ -4469,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 ""
@@ -4616,6 +4799,9 @@ msgstr ""
msgid "Retry verification"
msgstr ""
+msgid "Reveal Variables"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -4639,6 +4825,9 @@ msgstr ""
msgid "Revoke"
msgstr ""
+msgid "Run untagged jobs"
+msgstr ""
+
msgid "Runner token"
msgstr ""
@@ -4929,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 ""
@@ -5267,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 ""
@@ -5297,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 ""
@@ -5345,9 +5546,6 @@ msgstr ""
msgid "The time taken by each data entry gathered by that stage."
msgstr ""
-msgid "The update action will time out after 15 minutes. For big repositories, use a clone/push combination."
-msgstr ""
-
msgid "The user map is a JSON document mapping the Google Code users that participated on your projects to the way their email addresses and usernames will be imported into GitLab. You can change this by changing the value on the right hand side of <code>:</code>. Be sure to preserve the surrounding double quotes, other punctuation and the email address or username on the left hand side."
msgstr ""
@@ -5402,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 ""
@@ -5453,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 ""
@@ -5462,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 ""
@@ -5477,12 +5687,21 @@ 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 ""
+msgid "This runner will only run on pipelines triggered on protected branches"
+msgstr ""
+
msgid "This source diff could not be displayed because it is too large."
msgstr ""
+msgid "This timeout will take precedence when lower than Project-defined timeout"
+msgstr ""
+
msgid "This user has no identities"
msgstr ""
@@ -5724,6 +5943,9 @@ msgstr ""
msgid "ToggleButton|Toggle Status: ON"
msgstr ""
+msgid "Token"
+msgstr ""
+
msgid "Too many changes to show."
msgstr ""
@@ -5742,6 +5964,9 @@ msgstr ""
msgid "Trending"
msgstr ""
+msgid "Trigger"
+msgstr ""
+
msgid "Trigger this manual action"
msgstr ""
@@ -5760,6 +5985,9 @@ msgstr ""
msgid "Unlock"
msgstr ""
+msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
+msgstr ""
+
msgid "Unlocked"
msgstr ""
@@ -6150,6 +6378,9 @@ msgstr ""
msgid "You can resolve the merge conflict using either the Interactive mode, by choosing %{use_ours} or %{use_theirs} buttons, or by editing the files directly. Commit these changes into %{branch_name}"
msgstr ""
+msgid "You can setup jobs to only use Runners with specific tags. Separate tags with commas."
+msgstr ""
+
msgid "You cannot write to this read-only GitLab instance."
msgstr ""
@@ -6270,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 ""
@@ -6373,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 ""
@@ -6400,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 ""
@@ -6424,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 ""
@@ -6442,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/factories/uploads.rb b/spec/factories/uploads.rb
index a81b2169b89..81c485fba1a 100644
--- a/spec/factories/uploads.rb
+++ b/spec/factories/uploads.rb
@@ -46,6 +46,13 @@ FactoryBot.define do
secret SecureRandom.hex
end
+ trait :favicon_upload do
+ model { build(:appearance) }
+ path { File.join(secret, filename) }
+ uploader "FaviconUploader"
+ secret SecureRandom.hex
+ end
+
trait :attachment_upload do
transient do
mount_point :attachment
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/features/issues/award_emoji_spec.rb b/spec/features/issues/award_emoji_spec.rb
deleted file mode 100644
index bf60b18873c..00000000000
--- a/spec/features/issues/award_emoji_spec.rb
+++ /dev/null
@@ -1,146 +0,0 @@
-require 'rails_helper'
-
-describe 'Awards Emoji' do
- let!(:project) { create(:project, :public) }
- let!(:user) { create(:user) }
- let(:issue) do
- create(:issue,
- assignees: [user],
- project: project)
- end
-
- context 'authorized user' do
- before do
- project.add_maintainer(user)
- sign_in(user)
- end
-
- describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
- before do
- # The `heart_tip` emoji is not valid anymore so we need to skip validation
- issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
- it 'does not shows a 500 page', :js do
- expect(page).to have_text(issue.title)
- end
- end
-
- describe 'Click award emoji from issue#show' do
- let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
-
- before do
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'increments the thumbsdown emoji', :js do
- find('[data-name="thumbsdown"]').click
- wait_for_requests
- expect(thumbsdown_emoji).to have_text("1")
- end
-
- context 'click the thumbsup emoji' do
- it 'increments the thumbsup emoji', :js do
- find('[data-name="thumbsup"]').click
- wait_for_requests
- expect(thumbsup_emoji).to have_text("1")
- end
-
- it 'decrements the thumbsdown emoji', :js do
- expect(thumbsdown_emoji).to have_text("0")
- end
- end
-
- context 'click the thumbsdown emoji' do
- it 'increments the thumbsdown emoji', :js do
- find('[data-name="thumbsdown"]').click
- wait_for_requests
- expect(thumbsdown_emoji).to have_text("1")
- end
-
- it 'decrements the thumbsup emoji', :js do
- expect(thumbsup_emoji).to have_text("0")
- end
- end
-
- it 'toggles the smiley emoji on a note', :js do
- toggle_smiley_emoji(true)
-
- within('.note-body') do
- expect(find(emoji_counter)).to have_text("1")
- end
-
- toggle_smiley_emoji(false)
-
- within('.note-body') do
- expect(page).not_to have_selector(emoji_counter)
- end
- end
-
- context 'execute /award quick action' do
- it 'toggles the emoji award on noteable', :js do
- execute_quick_action('/award :100:')
-
- expect(find(noteable_award_counter)).to have_text("1")
-
- execute_quick_action('/award :100:')
-
- expect(page).not_to have_selector(noteable_award_counter)
- end
- end
- end
- end
-
- context 'unauthorized user', :js do
- before do
- visit project_issue_path(project, issue)
- end
-
- it 'has disabled emoji button' do
- expect(first('.award-control')[:class]).to have_text('disabled')
- end
- end
-
- def execute_quick_action(cmd)
- within('.js-main-target-form') do
- fill_in 'note[note]', with: cmd
- click_button 'Comment'
- end
-
- wait_for_requests
- end
-
- def thumbsup_emoji
- page.all(emoji_counter).first
- end
-
- def thumbsdown_emoji
- page.all(emoji_counter).last
- end
-
- def emoji_counter
- 'span.js-counter'
- end
-
- def noteable_award_counter
- ".awards .active"
- end
-
- def toggle_smiley_emoji(status)
- within('.note') do
- find('.note-emoji-button').click
- end
-
- unless status
- first('[data-name="smiley"]').click
- else
- find('[data-name="smiley"]').click
- end
-
- wait_for_requests
- end
-end
diff --git a/spec/features/issues/award_spec.rb b/spec/features/issues/award_spec.rb
deleted file mode 100644
index e53a4ce49c7..00000000000
--- a/spec/features/issues/award_spec.rb
+++ /dev/null
@@ -1,51 +0,0 @@
-require 'rails_helper'
-
-describe 'Issue awards', :js do
- let(:user) { create(:user) }
- let(:project) { create(:project, :public) }
- let(:issue) { create(:issue, project: project) }
-
- describe 'logged in' do
- before do
- sign_in(user)
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'adds award to issue' do
- first('.js-emoji-btn').click
- expect(page).to have_selector('.js-emoji-btn.active')
- expect(first('.js-emoji-btn')).to have_content '1'
-
- visit project_issue_path(project, issue)
- expect(first('.js-emoji-btn')).to have_content '1'
- end
-
- it 'removes award from issue' do
- first('.js-emoji-btn').click
- find('.js-emoji-btn.active').click
- expect(first('.js-emoji-btn')).to have_content '0'
-
- visit project_issue_path(project, issue)
- expect(first('.js-emoji-btn')).to have_content '0'
- end
-
- it 'only has one menu on the page' do
- first('.js-add-award').click
- expect(page).to have_selector('.emoji-menu')
-
- expect(page).to have_selector('.emoji-menu', count: 1)
- end
- end
-
- describe 'logged out' do
- before do
- visit project_issue_path(project, issue)
- wait_for_requests
- end
-
- it 'does not see award menu button' do
- expect(page).not_to have_selector('.js-award-holder')
- end
- end
-end
diff --git a/spec/features/issues/user_interacts_with_awards_spec.rb b/spec/features/issues/user_interacts_with_awards_spec.rb
new file mode 100644
index 00000000000..afa425c2cec
--- /dev/null
+++ b/spec/features/issues/user_interacts_with_awards_spec.rb
@@ -0,0 +1,347 @@
+require 'spec_helper'
+
+describe 'User interacts with awards' do
+ let(:user) { create(:user) }
+
+ describe 'User interacts with awards in an issue', :js do
+ let(:issue) { create(:issue, project: project)}
+ let(:project) { create(:project) }
+
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+
+ visit(project_issue_path(project, issue))
+ end
+
+ it 'toggles the thumbsup award emoji' do
+ page.within('.awards') do
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.award-control.js-emoji-btn')
+ expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
+
+ page.all('.award-control.js-emoji-btn').each do |element|
+ expect(element['title']).to eq('')
+ end
+
+ expect(page.all('.award-control .js-counter')).to all(have_content('0'))
+
+ thumbsup = page.first('.award-control')
+ thumbsup.click
+ thumbsup.hover
+
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+ end
+ end
+
+ it 'toggles a custom award emoji' do
+ page.within('.awards') do
+ page.find('.js-add-award').click
+ end
+
+ page.find('.emoji-menu.is-visible')
+
+ expect(page).to have_selector('.js-emoji-menu-search')
+ expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
+
+ page.within('.emoji-menu-content') do
+ emoji_button = page.first('.js-emoji-btn')
+ emoji_button.hover
+ emoji_button.click
+ end
+
+ page.within('.awards') do
+ expect(page).to have_selector('.js-emoji-btn')
+ expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
+ expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
+
+ expect do
+ page.find('.js-emoji-btn.active').click
+ wait_for_requests
+ end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2)
+ end
+ end
+
+ it 'shows the list of award emoji categories' do
+ page.within('.awards') do
+ page.find('.js-add-award').click
+ end
+
+ page.find('.emoji-menu.is-visible')
+
+ expect(page).to have_selector('.js-emoji-menu-search')
+ expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
+
+ fill_in('emoji-menu-search', with: 'hand')
+
+ page.within('.emoji-menu-content') do
+ expect(page).to have_selector('[data-name="raised_hand"]')
+ end
+ end
+
+ it 'adds an award emoji by a comment' do
+ page.within('.js-main-target-form') do
+ fill_in('note[note]', with: ':smile:')
+
+ click_button('Comment')
+ end
+
+ expect(page).to have_emoji('smile')
+ end
+
+ context 'when a project is archived' do
+ let(:project) { create(:project, :archived) }
+
+ it 'hides the add award button' do
+ page.within('.awards') do
+ expect(page).not_to have_css('.js-add-award')
+ end
+ end
+ end
+
+ context 'User interacts with awards on a note' do
+ let!(:note) { create(:note, noteable: issue, project: issue.project) }
+ let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
+
+ it 'shows the award on the note' do
+ page.within('.note-awards') do
+ expect(page).to have_emoji('100')
+ end
+ end
+
+ it 'allows adding a vote to an award' do
+ page.within('.note-awards') do
+ find('gl-emoji[data-name="100"]').click
+ end
+ wait_for_requests
+
+ expect(note.reload.award_emoji.size).to eq(2)
+ end
+
+ it 'allows adding a new emoji' do
+ page.within('.note-actions') do
+ find('a.js-add-award').click
+ end
+ page.within('.emoji-menu-content') do
+ find('gl-emoji[data-name="8ball"]').click
+ end
+ wait_for_requests
+
+ page.within('.note-awards') do
+ expect(page).to have_emoji('8ball')
+ end
+ expect(note.reload.award_emoji.size).to eq(2)
+ end
+
+ context 'when the project is archived' do
+ let(:project) { create(:project, :archived) }
+
+ it 'hides the buttons for adding new emoji' do
+ page.within('.note-awards') do
+ expect(page).not_to have_css('.award-menu-holder')
+ end
+
+ page.within('.note-actions') do
+ expect(page).not_to have_css('a.js-add-award')
+ end
+ end
+
+ it 'does not allow toggling existing emoji' do
+ page.within('.note-awards') do
+ find('gl-emoji[data-name="100"]').click
+ end
+ wait_for_requests
+
+ expect(note.reload.award_emoji.size).to eq(1)
+ end
+ end
+ end
+ end
+
+ describe 'User interacts with awards on an issue', :js do
+ let(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, project: project) }
+
+ describe 'logged in' do
+ before do
+ sign_in(user)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'adds award to issue' do
+ first('.js-emoji-btn').click
+
+ expect(page).to have_selector('.js-emoji-btn.active')
+ expect(first('.js-emoji-btn')).to have_content '1'
+
+ visit project_issue_path(project, issue)
+
+ expect(first('.js-emoji-btn')).to have_content '1'
+ end
+
+ it 'removes award from issue' do
+ first('.js-emoji-btn').click
+ find('.js-emoji-btn.active').click
+
+ expect(first('.js-emoji-btn')).to have_content '0'
+
+ visit project_issue_path(project, issue)
+
+ expect(first('.js-emoji-btn')).to have_content '0'
+ end
+
+ it 'only has one menu on the page' do
+ first('.js-add-award').click
+
+ expect(page).to have_selector('.emoji-menu', count: 1)
+ end
+ end
+
+ describe 'logged out' do
+ before do
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ it 'does not see award menu button' do
+ expect(page).not_to have_selector('.js-award-holder')
+ end
+ end
+ end
+
+ describe 'Awards Emoji' do
+ let!(:project) { create(:project, :public) }
+ let(:issue) { create(:issue, assignees: [user], project: project) }
+
+ context 'authorized user' do
+ before do
+ project.add_maintainer(user)
+ sign_in(user)
+ end
+
+ describe 'visiting an issue with a legacy award emoji that is not valid anymore' do
+ before do
+ # The `heart_tip` emoji is not valid anymore so we need to skip validation
+ issue.award_emoji.build(user: user, name: 'heart_tip').save!(validate: false)
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ # Regression test: https://gitlab.com/gitlab-org/gitlab-ce/issues/29529
+ it 'does not shows a 500 page', :js do
+ expect(page).to have_text(issue.title)
+ end
+ end
+
+ describe 'Click award emoji from issue#show' do
+ let!(:note) { create(:note_on_issue, noteable: issue, project: issue.project, note: "Hello world") }
+
+ before do
+ visit project_issue_path(project, issue)
+ wait_for_requests
+ end
+
+ context 'click the thumbsdown emoji' do
+ it 'increments the thumbsdown emoji', :js do
+ find('[data-name="thumbsdown"]').click
+ wait_for_requests
+ expect(thumbsdown_emoji).to have_text("1")
+ end
+
+ it 'decrements the thumbsup emoji', :js do
+ expect(thumbsup_emoji).to have_text("0")
+ end
+ end
+
+ it 'toggles the smiley emoji on a note', :js do
+ toggle_smiley_emoji(true)
+
+ within('.note-body') do
+ expect(find(emoji_counter)).to have_text("1")
+ end
+
+ toggle_smiley_emoji(false)
+
+ within('.note-body') do
+ expect(page).not_to have_selector(emoji_counter)
+ end
+ end
+
+ context 'execute /award quick action' do
+ it 'toggles the emoji award on noteable', :js do
+ execute_quick_action('/award :100:')
+
+ expect(find(noteable_award_counter)).to have_text("1")
+
+ execute_quick_action('/award :100:')
+
+ expect(page).not_to have_selector(noteable_award_counter)
+ end
+ end
+ end
+ end
+
+ context 'unauthorized user', :js do
+ before do
+ visit project_issue_path(project, issue)
+ end
+
+ it 'has disabled emoji button' do
+ expect(first('.award-control')[:class]).to have_text('disabled')
+ end
+ end
+
+ def execute_quick_action(cmd)
+ within('.js-main-target-form') do
+ fill_in 'note[note]', with: cmd
+ click_button 'Comment'
+ end
+
+ wait_for_requests
+ end
+
+ def thumbsup_emoji
+ page.all(emoji_counter).first
+ end
+
+ def thumbsdown_emoji
+ page.all(emoji_counter).last
+ end
+
+ def emoji_counter
+ 'span.js-counter'
+ end
+
+ def noteable_award_counter
+ ".awards .active"
+ end
+
+ def toggle_smiley_emoji(status)
+ within('.note') do
+ find('.note-emoji-button').click
+ end
+
+ if !status
+ first('[data-name="smiley"]').click
+ else
+ find('[data-name="smiley"]').click
+ end
+
+ wait_for_requests
+ end
+ end
+end
diff --git a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
index c1608be402a..fd4175d5227 100644
--- a/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
+++ b/spec/features/merge_request/user_sees_mr_with_deleted_source_branch_spec.rb
@@ -28,7 +28,7 @@ describe 'Merge request > User sees MR with deleted source branch', :js do
click_on 'Changes'
wait_for_requests
- expect(page).to have_selector('.diffs.tab-pane .nothing-here-block')
+ expect(page).to have_selector('.diffs.tab-pane .file-holder')
expect(page).to have_content('Source branch does not exist.')
end
end
diff --git a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb b/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
deleted file mode 100644
index 4d860893abe..00000000000
--- a/spec/features/projects/awards/user_interacts_with_awards_in_issue_spec.rb
+++ /dev/null
@@ -1,172 +0,0 @@
-require 'spec_helper'
-
-describe 'User interacts with awards in an issue', :js do
- let(:issue) { create(:issue, project: project)}
- let(:project) { create(:project) }
- let(:user) { create(:user) }
-
- before do
- project.add_maintainer(user)
- sign_in(user)
-
- visit(project_issue_path(project, issue))
- end
-
- it 'toggles the thumbsup award emoji' do
- page.within('.awards') do
- thumbsup = page.first('.award-control')
- thumbsup.click
- thumbsup.hover
-
- expect(page).to have_selector('.js-emoji-btn')
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
- expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
-
- thumbsup = page.first('.award-control')
- thumbsup.click
- thumbsup.hover
-
- expect(page).to have_selector('.award-control.js-emoji-btn')
- expect(page.all('.award-control.js-emoji-btn').size).to eq(2)
-
- page.all('.award-control.js-emoji-btn').each do |element|
- expect(element['title']).to eq('')
- end
-
- page.all('.award-control .js-counter').each do |element|
- expect(element).to have_content('0')
- end
-
- thumbsup = page.first('.award-control')
- thumbsup.click
- thumbsup.hover
-
- expect(page).to have_selector('.js-emoji-btn')
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
- expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
- end
- end
-
- it 'toggles a custom award emoji' do
- page.within('.awards') do
- page.find('.js-add-award').click
- end
-
- page.find('.emoji-menu.is-visible')
-
- expect(page).to have_selector('.js-emoji-menu-search')
- expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
-
- page.within('.emoji-menu-content') do
- emoji_button = page.first('.js-emoji-btn')
- emoji_button.hover
- emoji_button.click
- end
-
- page.within('.awards') do
- expect(page).to have_selector('.js-emoji-btn')
- expect(page.find('.js-emoji-btn.active .js-counter')).to have_content('1')
- expect(page).to have_css(".js-emoji-btn.active[data-original-title='You']")
-
- expect do
- page.find('.js-emoji-btn.active').click
- wait_for_requests
- end.to change { page.all('.award-control.js-emoji-btn').size }.from(3).to(2)
- end
- end
-
- it 'shows the list of award emoji categories' do
- page.within('.awards') do
- page.find('.js-add-award').click
- end
-
- page.find('.emoji-menu.is-visible')
-
- expect(page).to have_selector('.js-emoji-menu-search')
- expect(page.evaluate_script("document.activeElement.classList.contains('js-emoji-menu-search')")).to eq(true)
-
- fill_in('emoji-menu-search', with: 'hand')
-
- page.within('.emoji-menu-content') do
- expect(page).to have_selector('[data-name="raised_hand"]')
- end
- end
-
- it 'adds an award emoji by a comment' do
- page.within('.js-main-target-form') do
- fill_in('note[note]', with: ':smile:')
-
- click_button('Comment')
- end
-
- expect(page).to have_emoji('smile')
- end
-
- context 'when a project is archived' do
- let(:project) { create(:project, :archived) }
-
- it 'hides the add award button' do
- page.within('.awards') do
- expect(page).not_to have_css('.js-add-award')
- end
- end
- end
-
- context 'awards on a note' do
- let!(:note) { create(:note, noteable: issue, project: issue.project) }
- let!(:award_emoji) { create(:award_emoji, awardable: note, name: '100') }
-
- it 'shows the award on the note' do
- page.within('.note-awards') do
- expect(page).to have_emoji('100')
- end
- end
-
- it 'allows adding a vote to an award' do
- page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
- end
- wait_for_requests
-
- expect(note.reload.award_emoji.size).to eq(2)
- end
-
- it 'allows adding a new emoji' do
- page.within('.note-actions') do
- find('a.js-add-award').click
- end
- page.within('.emoji-menu-content') do
- find('gl-emoji[data-name="8ball"]').click
- end
- wait_for_requests
-
- page.within('.note-awards') do
- expect(page).to have_emoji('8ball')
- end
- expect(note.reload.award_emoji.size).to eq(2)
- end
-
- context 'when the project is archived' do
- let(:project) { create(:project, :archived) }
-
- it 'hides the buttons for adding new emoji' do
- page.within('.note-awards') do
- expect(page).not_to have_css('.award-menu-holder')
- end
-
- page.within('.note-actions') do
- expect(page).not_to have_css('a.js-add-award')
- end
- end
-
- it 'does not allow toggling existing emoji' do
- page.within('.note-awards') do
- find('gl-emoji[data-name="100"]').click
- end
- wait_for_requests
-
- expect(note.reload.award_emoji.size).to eq(1)
- end
- end
- end
-end
diff --git a/spec/finders/license_template_finder_spec.rb b/spec/finders/license_template_finder_spec.rb
new file mode 100644
index 00000000000..a97903103c9
--- /dev/null
+++ b/spec/finders/license_template_finder_spec.rb
@@ -0,0 +1,49 @@
+require 'spec_helper'
+
+describe LicenseTemplateFinder do
+ describe '#execute' do
+ subject(:result) { described_class.new(params).execute }
+
+ let(:categories) { categorised_licenses.keys }
+ let(:categorised_licenses) { result.group_by(&:category) }
+
+ context 'popular: true' do
+ let(:params) { { popular: true } }
+
+ it 'only returns popular licenses' do
+ expect(categories).to contain_exactly(:Popular)
+ expect(categorised_licenses[:Popular]).to be_present
+ end
+ end
+
+ context 'popular: false' do
+ let(:params) { { popular: false } }
+
+ it 'only returns unpopular licenses' do
+ expect(categories).to contain_exactly(:Other)
+ expect(categorised_licenses[:Other]).to be_present
+ end
+ end
+
+ context 'popular: nil' do
+ let(:params) { { popular: nil } }
+
+ it 'returns all licenses known by the Licensee gem' do
+ from_licensee = Licensee::License.all.map { |l| l.key }
+
+ expect(result.map(&:id)).to match_array(from_licensee)
+ end
+
+ it 'correctly copies all attributes' do
+ licensee = Licensee::License.all.first
+ found = result.find { |r| r.key == licensee.key }
+
+ aggregate_failures do
+ %i[key name content nickname url meta featured?].each do |k|
+ expect(found.public_send(k)).to eq(licensee.public_send(k))
+ end
+ end
+ end
+ end
+ end
+end
diff --git a/spec/helpers/button_helper_spec.rb b/spec/helpers/button_helper_spec.rb
index 630f3eff258..0c0a0003231 100644
--- a/spec/helpers/button_helper_spec.rb
+++ b/spec/helpers/button_helper_spec.rb
@@ -79,6 +79,18 @@ describe ButtonHelper do
end
end
+ context 'without an ssh key on the user and user_show_add_ssh_key_message unset' do
+ before do
+ stub_application_setting(user_show_add_ssh_key_message: false)
+ end
+
+ it 'there is no warning on the dropdown description' do
+ description = element.search('.dropdown-menu-inner-content').first
+
+ expect(description).to be_nil
+ end
+ end
+
context 'with an ssh key on the user' do
before do
create(:key, user: user)
diff --git a/spec/helpers/icons_helper_spec.rb b/spec/helpers/icons_helper_spec.rb
index 82f588d1a08..4b40d523287 100644
--- a/spec/helpers/icons_helper_spec.rb
+++ b/spec/helpers/icons_helper_spec.rb
@@ -80,6 +80,26 @@ describe IconsHelper do
end
end
+ describe 'audit icon' do
+ it 'returns right icon name for standard auth' do
+ icon_name = 'standard'
+ expect(audit_icon(icon_name).to_s)
+ .to eq '<i class="fa fa-key"></i>'
+ end
+
+ it 'returns right icon name for two-factor auth' do
+ icon_name = 'two-factor'
+ expect(audit_icon(icon_name).to_s)
+ .to eq '<i class="fa fa-key"></i>'
+ end
+
+ it 'returns right icon name for google_oauth2 auth' do
+ icon_name = 'google_oauth2'
+ expect(audit_icon(icon_name).to_s)
+ .to eq '<i class="fa fa-google"></i>'
+ end
+ end
+
describe 'file_type_icon_class' do
it 'returns folder class' do
expect(file_type_icon_class('folder', 0, 'folder_name')).to eq 'folder'
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/diffs/components/diff_file_spec.js b/spec/javascripts/diffs/components/diff_file_spec.js
index 7a4616ec8eb..44a38f7ca82 100644
--- a/spec/javascripts/diffs/components/diff_file_spec.js
+++ b/spec/javascripts/diffs/components/diff_file_spec.js
@@ -22,11 +22,18 @@ describe('DiffFile', () => {
expect(el.id).toEqual(fileHash);
expect(el.classList.contains('diff-file')).toEqual(true);
+
expect(el.querySelectorAll('.diff-content.hidden').length).toEqual(0);
expect(el.querySelector('.js-file-title')).toBeDefined();
expect(el.querySelector('.file-title-name').innerText.indexOf(filePath) > -1).toEqual(true);
expect(el.querySelector('.js-syntax-highlight')).toBeDefined();
- expect(el.querySelectorAll('.line_content').length > 5).toEqual(true);
+
+ expect(vm.file.renderIt).toEqual(false);
+ vm.file.renderIt = true;
+
+ vm.$nextTick(() => {
+ expect(el.querySelectorAll('.line_content').length > 5).toEqual(true);
+ });
});
describe('collapsed', () => {
@@ -34,6 +41,7 @@ describe('DiffFile', () => {
expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(1);
expect(vm.file.collapsed).toEqual(false);
vm.file.collapsed = true;
+ vm.file.renderIt = true;
vm.$nextTick(() => {
expect(vm.$el.querySelectorAll('.diff-content').length).toEqual(0);
diff --git a/spec/javascripts/diffs/mock_data/diff_file.js b/spec/javascripts/diffs/mock_data/diff_file.js
index d3bf9525924..cce36ecc91f 100644
--- a/spec/javascripts/diffs/mock_data/diff_file.js
+++ b/spec/javascripts/diffs/mock_data/diff_file.js
@@ -39,6 +39,7 @@ export default {
viewPath: '/gitlab-org/gitlab-test/blob/spooky-stuff/CHANGELOG',
replacedViewPath: null,
collapsed: false,
+ renderIt: false,
tooLarge: false,
contextLinesPath:
'/gitlab-org/gitlab-test/blob/c48ee0d1bf3b30453f5b32250ce03134beaa6d13/CHANGELOG/diff',
diff --git a/spec/javascripts/diffs/store/mutations_spec.js b/spec/javascripts/diffs/store/mutations_spec.js
index 1af49f4985c..8f89984c6e5 100644
--- a/spec/javascripts/diffs/store/mutations_spec.js
+++ b/spec/javascripts/diffs/store/mutations_spec.js
@@ -1,6 +1,7 @@
import mutations from '~/diffs/store/mutations';
import * as types from '~/diffs/store/mutation_types';
import { INLINE_DIFF_VIEW_TYPE } from '~/diffs/constants';
+import diffFileMockData from '../mock_data/diff_file';
describe('DiffsStoreMutations', () => {
describe('SET_BASE_CONFIG', () => {
@@ -24,6 +25,23 @@ describe('DiffsStoreMutations', () => {
});
});
+ describe('SET_DIFF_DATA', () => {
+ it('should set diff data type properly', () => {
+ const state = {};
+ const diffMock = {
+ diff_files: [diffFileMockData],
+ };
+
+ mutations[types.SET_DIFF_DATA](state, diffMock);
+
+ const firstLine = state.diffFiles[0].parallelDiffLines[0];
+
+ expect(firstLine.right.text).toBeUndefined();
+ expect(state.diffFiles[0].renderIt).toEqual(true);
+ expect(state.diffFiles[0].collapsed).toEqual(false);
+ });
+ });
+
describe('SET_DIFF_VIEW_TYPE', () => {
it('should set diff view type properly', () => {
const state = {};
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/ide/stores/mutations_spec.js b/spec/javascripts/ide/stores/mutations_spec.js
index 1e836dbc3f9..6ce76aaa03b 100644
--- a/spec/javascripts/ide/stores/mutations_spec.js
+++ b/spec/javascripts/ide/stores/mutations_spec.js
@@ -213,6 +213,33 @@ describe('Multi-file store mutations', () => {
expect(localState.changedFiles).toEqual([localState.entries.filePath]);
});
+
+ it('does not add tempFile into changedFiles', () => {
+ localState.entries.filePath = {
+ deleted: false,
+ type: 'blob',
+ tempFile: true,
+ };
+
+ mutations.DELETE_ENTRY(localState, 'filePath');
+
+ expect(localState.changedFiles).toEqual([]);
+ });
+
+ it('removes tempFile from changedFiles when deleted', () => {
+ localState.entries.filePath = {
+ path: 'filePath',
+ deleted: false,
+ type: 'blob',
+ tempFile: true,
+ };
+
+ localState.changedFiles.push({ ...localState.entries.filePath });
+
+ mutations.DELETE_ENTRY(localState, 'filePath');
+
+ expect(localState.changedFiles).toEqual([]);
+ });
});
describe('UPDATE_FILE_AFTER_COMMIT', () => {
diff --git a/spec/javascripts/jobs/artifacts_block_spec.js b/spec/javascripts/jobs/artifacts_block_spec.js
new file mode 100644
index 00000000000..c544c6f3e89
--- /dev/null
+++ b/spec/javascripts/jobs/artifacts_block_spec.js
@@ -0,0 +1,120 @@
+import Vue from 'vue';
+import { getTimeago } from '~/lib/utils/datetime_utility';
+import component from '~/jobs/components/artifacts_block.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Artifacts block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const expireAt = '2018-08-14T09:38:49.157Z';
+ const timeago = getTimeago();
+ const formatedDate = timeago.format(expireAt);
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('with expired artifacts', () => {
+ it('renders expired artifact date and info', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ });
+
+ expect(vm.$el.querySelector('.js-artifacts-removed')).not.toBeNull();
+ expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).toBeNull();
+ expect(vm.$el.textContent).toContain(formatedDate);
+ });
+ });
+
+ describe('with artifacts that will expire', () => {
+ it('renders will expire artifact date and info', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: false,
+ willArtifactsExpire: true,
+ expireAt,
+ });
+
+ expect(vm.$el.querySelector('.js-artifacts-removed')).toBeNull();
+ expect(vm.$el.querySelector('.js-artifacts-will-be-removed')).not.toBeNull();
+ expect(vm.$el.textContent).toContain(formatedDate);
+ });
+ });
+
+ describe('when the user can keep the artifacts', () => {
+ it('renders the keep button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ keepArtifactsPath: '/keep',
+ });
+
+ expect(vm.$el.querySelector('.js-keep-artifacts')).not.toBeNull();
+ });
+ });
+
+ describe('when the user can not keep the artifacts', () => {
+ it('does not render the keep button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ });
+
+ expect(vm.$el.querySelector('.js-keep-artifacts')).toBeNull();
+ });
+ });
+
+ describe('when the user can download the artifacts', () => {
+ it('renders the download button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ downloadArtifactsPath: '/download',
+ });
+
+ expect(vm.$el.querySelector('.js-download-artifacts')).not.toBeNull();
+ });
+ });
+
+ describe('when the user can not download the artifacts', () => {
+ it('does not render the keep button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ });
+
+ expect(vm.$el.querySelector('.js-download-artifacts')).toBeNull();
+ });
+ });
+
+ describe('when the user can browse the artifacts', () => {
+ it('does not render the browse button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ browseArtifactsPath: '/browse',
+ });
+
+ expect(vm.$el.querySelector('.js-browse-artifacts')).not.toBeNull();
+ });
+ });
+
+ describe('when the user can not browse the artifacts', () => {
+ it('does not render the browse button', () => {
+ vm = mountComponent(Component, {
+ haveArtifactsExpired: true,
+ willArtifactsExpire: false,
+ expireAt,
+ });
+
+ expect(vm.$el.querySelector('.js-browse-artifacts')).toBeNull();
+ });
+ });
+});
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/empty_state_spec.js b/spec/javascripts/jobs/empty_state_spec.js
new file mode 100644
index 00000000000..f8feb069fe0
--- /dev/null
+++ b/spec/javascripts/jobs/empty_state_spec.js
@@ -0,0 +1,90 @@
+import Vue from 'vue';
+import component from '~/jobs/components/empty_state.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Empty State', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const props = {
+ illustrationPath: 'illustrations/pending_job_empty.svg',
+ illustrationSizeClass: 'svg-430',
+ title: 'This job has not started yet',
+ };
+
+ const content = 'This job is in pending state and is waiting to be picked by a runner';
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('renders image and title', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ });
+ });
+
+ it('renders img with provided path and size', () => {
+ expect(vm.$el.querySelector('img').getAttribute('src')).toEqual(props.illustrationPath);
+ expect(vm.$el.querySelector('.svg-content').classList).toContain(props.illustrationSizeClass);
+ });
+
+ it('renders provided title', () => {
+ expect(vm.$el.querySelector('.js-job-empty-state-title').textContent.trim()).toEqual(
+ props.title,
+ );
+ });
+ });
+
+ describe('with content', () => {
+ it('renders content', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ });
+
+ expect(vm.$el.querySelector('.js-job-empty-state-content').textContent.trim()).toEqual(
+ content,
+ );
+ });
+ });
+
+ describe('without content', () => {
+ it('does not render content', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+ expect(vm.$el.querySelector('.js-job-empty-state-content')).toBeNull();
+ });
+ });
+
+ describe('with action', () => {
+ it('renders action', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ action: {
+ link: 'runner',
+ title: 'Check runner',
+ method: 'post',
+ },
+ });
+
+ expect(vm.$el.querySelector('.js-job-empty-state-action').getAttribute('href')).toEqual(
+ 'runner',
+ );
+ });
+ });
+
+ describe('without action', () => {
+ it('does not render action', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ content,
+ });
+ expect(vm.$el.querySelector('.js-job-empty-state-action')).toBeNull();
+ });
+ });
+});
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/cleanup/project_uploads_spec.rb b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
index 37b38776775..11e605eece6 100644
--- a/spec/lib/gitlab/cleanup/project_uploads_spec.rb
+++ b/spec/lib/gitlab/cleanup/project_uploads_spec.rb
@@ -244,9 +244,11 @@ describe Gitlab::Cleanup::ProjectUploads do
orphaned1 = create(:upload, :personal_snippet_upload, :with_file)
orphaned2 = create(:upload, :namespace_upload, :with_file)
orphaned3 = create(:upload, :attachment_upload, :with_file)
+ orphaned4 = create(:upload, :favicon_upload, :with_file)
paths << orphaned1.absolute_path
paths << orphaned2.absolute_path
paths << orphaned3.absolute_path
+ paths << orphaned4.absolute_path
Upload.delete_all
expect(logger).not_to receive(:info).with(/move|fix/i)
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/gitlab/template/finders/repo_template_finders_spec.rb b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb
new file mode 100644
index 00000000000..2eabccd5dff
--- /dev/null
+++ b/spec/lib/gitlab/template/finders/repo_template_finders_spec.rb
@@ -0,0 +1,37 @@
+require 'spec_helper'
+
+describe Gitlab::Template::Finders::RepoTemplateFinder do
+ set(:project) { create(:project, :repository) }
+
+ let(:categories) { { 'HTML' => 'html' } }
+
+ subject(:finder) { described_class.new(project, 'files/', '.html', categories) }
+
+ describe '#read' do
+ it 'returns the content of the given path' do
+ result = finder.read('files/html/500.html')
+
+ expect(result).to be_present
+ end
+
+ it 'raises an error if the path does not exist' do
+ expect { finder.read('does/not/exist') }.to raise_error(described_class::FileNotFoundError)
+ end
+ end
+
+ describe '#find' do
+ it 'returns the full path of the found template' do
+ result = finder.find('500')
+
+ expect(result).to eq('files/html/500.html')
+ end
+ end
+
+ describe '#list_files_for' do
+ it 'returns the full path of the found files' do
+ result = finder.list_files_for('files/html')
+
+ expect(result).to contain_exactly('files/html/500.html')
+ end
+ end
+end
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/internal_id_spec.rb b/spec/models/internal_id_spec.rb
index 20600f5fa38..f2aad455d5f 100644
--- a/spec/models/internal_id_spec.rb
+++ b/spec/models/internal_id_spec.rb
@@ -30,7 +30,7 @@ describe InternalId do
context 'with existing issues' do
before do
- rand(1..10).times { create(:issue, project: project) }
+ create_list(:issue, 2, project: project)
described_class.delete_all
end
@@ -54,7 +54,7 @@ describe InternalId do
end
it 'generates a strictly monotone, gapless sequence' do
- seq = (0..rand(100)).map do
+ seq = Array.new(10).map do
described_class.generate_next(issue, scope, usage, init)
end
normalized = seq.map { |i| i - seq.min }
diff --git a/spec/models/license_template_spec.rb b/spec/models/license_template_spec.rb
new file mode 100644
index 00000000000..c633e1908d4
--- /dev/null
+++ b/spec/models/license_template_spec.rb
@@ -0,0 +1,59 @@
+require 'spec_helper'
+
+describe LicenseTemplate do
+ describe '#content' do
+ it 'calls a proc exactly once if provided' do
+ lazy = build_template(-> { 'bar' })
+ content = lazy.content
+
+ expect(content).to eq('bar')
+ expect(content.object_id).to eq(lazy.content.object_id)
+
+ content.replace('foo')
+ expect(lazy.content).to eq('foo')
+ end
+
+ it 'returns a string if provided' do
+ lazy = build_template('bar')
+
+ expect(lazy.content).to eq('bar')
+ end
+ end
+
+ describe '#resolve!' do
+ let(:content) do
+ <<~TEXT
+ Pretend License
+
+ [project]
+
+ Copyright (c) [year] [fullname]
+ TEXT
+ end
+
+ let(:expected) do
+ <<~TEXT
+ Pretend License
+
+ Foo Project
+
+ Copyright (c) 1985 Nick Thomas
+ TEXT
+ end
+
+ let(:template) { build_template(content) }
+
+ it 'updates placeholders in a copy of the template content' do
+ expect(template.content.object_id).to eq(content.object_id)
+
+ template.resolve!(project_name: "Foo Project", fullname: "Nick Thomas", year: "1985")
+
+ expect(template.content).to eq(expected)
+ expect(template.content.object_id).not_to eq(content.object_id)
+ end
+ end
+
+ def build_template(content)
+ described_class.new(id: 'foo', name: 'foo', category: :Other, content: content)
+ end
+end
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/jobs_spec.rb b/spec/requests/api/jobs_spec.rb
index 5814d834572..6adbbb40489 100644
--- a/spec/requests/api/jobs_spec.rb
+++ b/spec/requests/api/jobs_spec.rb
@@ -3,6 +3,32 @@ require 'spec_helper'
describe API::Jobs do
include HttpIOHelpers
+ shared_examples 'a job with artifacts and trace' do |result_is_array: true|
+ context 'with artifacts and trace' do
+ let!(:second_job) { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) }
+
+ it 'returns artifacts and trace data', :skip_before_request do
+ get api(api_endpoint, api_user)
+ json_job = result_is_array ? json_response.select { |job| job['id'] == second_job.id }.first : json_response
+
+ expect(json_job['artifacts_file']).not_to be_nil
+ expect(json_job['artifacts_file']).not_to be_empty
+ expect(json_job['artifacts_file']['filename']).to eq(second_job.artifacts_file.filename)
+ expect(json_job['artifacts_file']['size']).to eq(second_job.artifacts_file.size)
+ expect(json_job['artifacts']).not_to be_nil
+ expect(json_job['artifacts']).to be_an Array
+ expect(json_job['artifacts'].size).to eq(second_job.job_artifacts.length)
+ json_job['artifacts'].each do |artifact|
+ expect(artifact).not_to be_nil
+ file_type = Ci::JobArtifact.file_types[artifact['file_type']]
+ expect(artifact['size']).to eq(second_job.job_artifacts.where(file_type: file_type).first.size)
+ expect(artifact['filename']).to eq(second_job.job_artifacts.where(file_type: file_type).first.filename)
+ expect(artifact['file_format']).to eq(second_job.job_artifacts.where(file_type: file_type).first.file_format)
+ end
+ end
+ end
+ end
+
set(:project) do
create(:project, :repository, public_builds: false)
end
@@ -49,6 +75,20 @@ describe API::Jobs do
expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
end
+ context 'without artifacts and trace' do
+ it 'returns no artifacts nor trace data' do
+ json_job = json_response.first
+
+ expect(json_job['artifacts_file']).to be_nil
+ expect(json_job['artifacts']).to be_an Array
+ expect(json_job['artifacts']).to be_empty
+ end
+ end
+
+ it_behaves_like 'a job with artifacts and trace' do
+ let(:api_endpoint) { "/projects/#{project.id}/jobs" }
+ end
+
it 'returns pipeline data' do
json_job = json_response.first
@@ -60,7 +100,7 @@ describe API::Jobs do
end
it 'avoids N+1 queries', :skip_before_request do
- first_build = create(:ci_build, :artifacts, pipeline: pipeline)
+ first_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline)
first_build.runner = create(:ci_runner)
first_build.user = create(:user)
first_build.save
@@ -68,7 +108,7 @@ describe API::Jobs do
control_count = ActiveRecord::QueryRecorder.new { go }.count
second_pipeline = create(:ci_empty_pipeline, project: project, sha: project.commit.id, ref: project.default_branch)
- second_build = create(:ci_build, :artifacts, pipeline: second_pipeline)
+ second_build = create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: second_pipeline)
second_build.runner = create(:ci_runner)
second_build.user = create(:user)
second_build.save
@@ -117,9 +157,11 @@ describe API::Jobs do
describe 'GET /projects/:id/pipelines/:pipeline_id/jobs' do
let(:query) { Hash.new }
- before do
- job
- get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ before do |example|
+ unless example.metadata[:skip_before_request]
+ job
+ get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
+ end
end
context 'authorized user' do
@@ -133,6 +175,13 @@ describe API::Jobs do
expect(json_response).not_to be_empty
expect(json_response.first['commit']['id']).to eq project.commit.id
expect(Time.parse(json_response.first['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response.first['artifacts_file']).to be_nil
+ expect(json_response.first['artifacts']).to be_an Array
+ expect(json_response.first['artifacts']).to be_empty
+ end
+
+ it_behaves_like 'a job with artifacts and trace' do
+ let(:api_endpoint) { "/projects/#{project.id}/pipelines/#{pipeline.id}/jobs" }
end
it 'returns pipeline data' do
@@ -183,7 +232,7 @@ describe API::Jobs do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
end.count
- 3.times { create(:ci_build, :artifacts, pipeline: pipeline) }
+ 3.times { create(:ci_build, :trace_artifact, :artifacts, :test_reports, pipeline: pipeline) }
expect do
get api("/projects/#{project.id}/pipelines/#{pipeline.id}/jobs", api_user), query
@@ -201,8 +250,10 @@ describe API::Jobs do
end
describe 'GET /projects/:id/jobs/:job_id' do
- before do
- get api("/projects/#{project.id}/jobs/#{job.id}", api_user)
+ before do |example|
+ unless example.metadata[:skip_before_request]
+ get api("/projects/#{project.id}/jobs/#{job.id}", api_user)
+ end
end
context 'authorized user' do
@@ -219,10 +270,17 @@ describe API::Jobs do
expect(Time.parse(json_response['started_at'])).to be_like_time(job.started_at)
expect(Time.parse(json_response['finished_at'])).to be_like_time(job.finished_at)
expect(Time.parse(json_response['artifacts_expire_at'])).to be_like_time(job.artifacts_expire_at)
+ expect(json_response['artifacts_file']).to be_nil
+ expect(json_response['artifacts']).to be_an Array
+ expect(json_response['artifacts']).to be_empty
expect(json_response['duration']).to eq(job.duration)
expect(json_response['web_url']).to be_present
end
+ it_behaves_like 'a job with artifacts and trace', result_is_array: false do
+ let(:api_endpoint) { "/projects/#{project.id}/jobs/#{second_job.id}" }
+ end
+
it 'returns pipeline data' do
json_job = json_response
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/requests/api/templates_spec.rb b/spec/requests/api/templates_spec.rb
index 6bb53fdc98d..d1e16ab9ca9 100644
--- a/spec/requests/api/templates_spec.rb
+++ b/spec/requests/api/templates_spec.rb
@@ -56,6 +56,8 @@ describe API::Templates do
end
it 'returns a license template' do
+ expect(response).to have_gitlab_http_status(200)
+
expect(json_response['key']).to eq('mit')
expect(json_response['name']).to eq('MIT License')
expect(json_response['nickname']).to be_nil
@@ -181,6 +183,7 @@ describe API::Templates do
it 'replaces the copyright owner placeholder with the name of the current user' do
get api('/templates/licenses/mit', user)
+ expect(response).to have_gitlab_http_status(200)
expect(json_response['content']).to include("Copyright (c) #{Time.now.year} #{user.name}")
end
end
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/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
index 39876805ffa..ffcf5648075 100644
--- a/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
+++ b/vendor/gitlab-ci-yml/Auto-DevOps.gitlab-ci.yml
@@ -642,9 +642,9 @@ rollout 100%:
function install_dependencies() {
apk add -U openssl curl tar gzip bash ca-certificates git
wget -q -O /etc/apk/keys/sgerrand.rsa.pub https://alpine-pkgs.sgerrand.com/sgerrand.rsa.pub
- wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.23-r3/glibc-2.23-r3.apk
- apk add glibc-2.23-r3.apk
- rm glibc-2.23-r3.apk
+ wget https://github.com/sgerrand/alpine-pkg-glibc/releases/download/2.28-r0/glibc-2.28-r0.apk
+ apk add glibc-2.28-r0.apk
+ rm glibc-2.28-r0.apk
curl "https://kubernetes-helm.storage.googleapis.com/helm-v${HELM_VERSION}-linux-amd64.tar.gz" | tar zx
mv linux-amd64/helm /usr/bin/
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: