summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--.gitlab-ci.yml10
-rw-r--r--CONTRIBUTING.md12
-rw-r--r--GITALY_SERVER_VERSION2
-rw-r--r--Gemfile5
-rw-r--r--Gemfile.lock9
-rw-r--r--Gemfile.rails5.lock9
-rw-r--r--README.md4
-rw-r--r--app/assets/javascripts/ide/components/repo_editor.vue1
-rw-r--r--app/assets/javascripts/ide/stores/actions/file.js7
-rw-r--r--app/assets/javascripts/importer_status.js2
-rw-r--r--app/assets/javascripts/jobs/components/commit_block.vue64
-rw-r--r--app/assets/javascripts/jobs/components/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/monitoring/components/dashboard.vue2
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/index.js (renamed from app/assets/javascripts/pages/admin/cohorts/index.js)0
-rw-r--r--app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js (renamed from app/assets/javascripts/pages/admin/cohorts/usage_ping.js)0
-rw-r--r--app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js (renamed from app/assets/javascripts/pages/admin/conversational_development_index/show/index.js)0
-rw-r--r--app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue3
-rw-r--r--app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue10
-rw-r--r--app/assets/stylesheets/framework/awards.scss4
-rw-r--r--app/assets/stylesheets/framework/common.scss19
-rw-r--r--app/assets/stylesheets/framework/files.scss10
-rw-r--r--app/assets/stylesheets/framework/images.scss2
-rw-r--r--app/assets/stylesheets/framework/issue_box.scss2
-rw-r--r--app/assets/stylesheets/framework/lists.scss18
-rw-r--r--app/assets/stylesheets/framework/mixins.scss2
-rw-r--r--app/assets/stylesheets/framework/typography.scss2
-rw-r--r--app/assets/stylesheets/framework/variables.scss29
-rw-r--r--app/assets/stylesheets/pages/boards.scss2
-rw-r--r--app/assets/stylesheets/pages/cycle_analytics.scss2
-rw-r--r--app/assets/stylesheets/pages/diff.scss2
-rw-r--r--app/assets/stylesheets/pages/events.scss4
-rw-r--r--app/assets/stylesheets/pages/groups.scss4
-rw-r--r--app/assets/stylesheets/pages/issuable.scss12
-rw-r--r--app/assets/stylesheets/pages/notes.scss3
-rw-r--r--app/assets/stylesheets/pages/profile.scss6
-rw-r--r--app/assets/stylesheets/pages/projects.scss6
-rw-r--r--app/assets/stylesheets/pages/settings.scss8
-rw-r--r--app/assets/stylesheets/pages/todos.scss6
-rw-r--r--app/assets/stylesheets/pages/tree.scss8
-rw-r--r--app/controllers/autocomplete_controller.rb72
-rw-r--r--app/controllers/projects/pages_controller.rb2
-rw-r--r--app/finders/autocomplete/group_finder.rb37
-rw-r--r--app/finders/autocomplete/move_to_project_finder.rb35
-rw-r--r--app/finders/autocomplete/project_finder.rb35
-rw-r--r--app/finders/autocomplete/users_finder.rb85
-rw-r--r--app/finders/autocomplete_users_finder.rb68
-rw-r--r--app/finders/awarded_emoji_finder.rb21
-rw-r--r--app/finders/move_to_project_finder.rb21
-rw-r--r--app/finders/user_finder.rb26
-rw-r--r--app/models/award_emoji.rb17
-rw-r--r--app/models/concerns/awardable.rb2
-rw-r--r--app/models/concerns/fast_destroy_all.rb2
-rw-r--r--app/models/concerns/optionally_search.rb19
-rw-r--r--app/models/internal_id.rb6
-rw-r--r--app/models/lfs_object.rb2
-rw-r--r--app/models/project.rb22
-rw-r--r--app/models/project_services/gemnasium_service.rb62
-rw-r--r--app/models/site_statistic.rb2
-rw-r--r--app/models/user.rb47
-rw-r--r--app/serializers/move_to_project_entity.rb6
-rw-r--r--app/serializers/move_to_project_serializer.rb5
-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/protected_branches/legacy_api_update_service.rb4
-rw-r--r--app/services/users/destroy_service.rb7
-rw-r--r--app/views/admin/application_settings/_ci_cd.html.haml2
-rw-r--r--app/views/admin/runners/index.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/projects/environments/metrics.html.haml7
-rw-r--r--app/views/projects/mirrors/_mirror_repos.html.haml2
-rw-r--r--app/views/projects/runners/_specific_runners.html.haml2
-rw-r--r--app/workers/remove_expired_group_links_worker.rb2
-rw-r--r--app/workers/remove_old_web_hook_logs_worker.rb2
-rw-r--r--changelogs/unreleased/42754-runners-pagination.yml5
-rw-r--r--changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml5
-rw-r--r--changelogs/unreleased/48145-illustration.yml5
-rw-r--r--changelogs/unreleased/48869-wiki-slugs-with-spaces.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-user-deletion.yml5
-rw-r--r--changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml5
-rw-r--r--changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml5
-rw-r--r--changelogs/unreleased/50101-builds-dropdown.yml6
-rw-r--r--changelogs/unreleased/50101-commit-block.yml5
-rw-r--r--changelogs/unreleased/50101-trigger.yml5
-rw-r--r--changelogs/unreleased/50312-instance-statistics-convdev-index-intro-banner-is-not-dismissable.yml5
-rw-r--r--changelogs/unreleased/6010_remove_gemnasium_service.yml5
-rw-r--r--changelogs/unreleased/ab-49446-internal-ids-inconsistency.yml5
-rw-r--r--changelogs/unreleased/emoji-cutoff-1px.yml5
-rw-r--r--changelogs/unreleased/fix_event_api_permissions.yml5
-rw-r--r--changelogs/unreleased/rails5-verbose-query-logs.yml5
-rw-r--r--changelogs/unreleased/repopulate_site_statistics.yml5
-rw-r--r--changelogs/unreleased/rouge_3-2-1.yml5
-rw-r--r--changelogs/unreleased/runners-online.yml5
-rw-r--r--changelogs/unreleased/sh-bump-gitaly-for-11-2.yml5
-rw-r--r--config/initializers/active_record_verbose_query_logs.rb4
-rw-r--r--config/initializers/gettext_rails_i18n_patch.rb15
-rw-r--r--db/migrate/20160712171823_remove_award_emojis_with_no_user.rb2
-rw-r--r--db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb47
-rw-r--r--db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb32
-rw-r--r--db/schema.rb2
-rw-r--r--doc/administration/high_availability/gitlab.md10
-rw-r--r--doc/administration/high_availability/nfs.md4
-rw-r--r--doc/administration/repository_storage_types.md2
-rw-r--r--doc/api/events.md12
-rw-r--r--doc/api/services.md42
-rw-r--r--doc/ci/README.md2
-rw-r--r--doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.pngbin0 -> 35571 bytes
-rw-r--r--doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.pngbin0 -> 23431 bytes
-rw-r--r--doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.pngbin0 -> 55682 bytes
-rw-r--r--doc/ci/interactive_web_terminal/index.md52
-rw-r--r--doc/ci/yaml/README.md4
-rw-r--r--doc/development/README.md8
-rw-r--r--doc/development/ee_features.md28
-rw-r--r--doc/development/testing_guide/smoke.md16
-rw-r--r--doc/development/testing_guide/testing_levels.md8
-rw-r--r--doc/development/understanding_explain_plans.md676
-rw-r--r--doc/install/kubernetes/gitlab_omnibus.md2
-rw-r--r--doc/integration/README.md2
-rw-r--r--doc/user/admin_area/settings/continuous_integration.md5
-rw-r--r--doc/user/markdown.md15
-rw-r--r--doc/user/project/integrations/hangouts_chat.md2
-rw-r--r--doc/user/project/integrations/project_services.md1
-rw-r--r--doc/user/project/integrations/webhooks.md1
-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--lib/api/events.rb5
-rw-r--r--lib/api/services.rb15
-rw-r--r--lib/banzai/filter/spaced_link_filter.rb77
-rw-r--r--lib/banzai/pipeline/wiki_pipeline.rb1
-rw-r--r--lib/gitlab/data_builder/build.rb1
-rw-r--r--lib/gitlab/git/diff.rb1
-rw-r--r--lib/gitlab/git/repository.rb10
-rw-r--r--lib/gitlab/gitaly_client/diff.rb2
-rw-r--r--lib/gitlab/github_import/bulk_importing.rb4
-rw-r--r--lib/gitlab/github_import/importer/diff_note_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/issue_importer.rb8
-rw-r--r--lib/gitlab/github_import/importer/milestones_importer.rb12
-rw-r--r--lib/gitlab/github_import/importer/note_importer.rb2
-rw-r--r--lib/gitlab/github_import/importer/pull_request_importer.rb10
-rw-r--r--lib/gitlab/hook_data/issuable_builder.rb2
-rw-r--r--lib/gitlab/hook_data/issue_builder.rb66
-rw-r--r--lib/gitlab/hook_data/merge_request_builder.rb60
-rw-r--r--lib/gitlab/import_export/members_mapper.rb2
-rw-r--r--lib/tasks/gitlab/site_statistics.rake23
-rw-r--r--locale/gitlab.pot194
-rw-r--r--package.json2
-rw-r--r--qa/README.md12
-rw-r--r--qa/qa.rb6
-rw-r--r--qa/qa/factory/resource/fork.rb7
-rw-r--r--qa/qa/factory/resource/user.rb48
-rw-r--r--qa/qa/git/repository.rb2
-rw-r--r--qa/qa/page/main/login.rb25
-rw-r--r--qa/qa/page/main/sign_up.rb12
-rw-r--r--qa/qa/runtime/env.rb12
-rw-r--r--qa/qa/runtime/user.rb6
-rw-r--r--qa/qa/scenario/taggable.rb17
-rw-r--r--qa/qa/scenario/template.rb33
-rw-r--r--qa/qa/scenario/test/instance.rb36
-rw-r--r--qa/qa/scenario/test/instance/all.rb15
-rw-r--r--qa/qa/scenario/test/instance/smoke.rb17
-rw-r--r--qa/qa/specs/features/api/basics_spec.rb4
-rw-r--r--qa/qa/specs/features/api/users_spec.rb6
-rw-r--r--qa/qa/specs/features/login/basic_spec.rb15
-rw-r--r--qa/qa/specs/features/login/ldap_spec.rb2
-rw-r--r--qa/qa/specs/features/mattermost/group_create_spec.rb2
-rw-r--r--qa/qa/specs/features/mattermost/login_spec.rb2
-rw-r--r--qa/qa/specs/features/merge_request/create_spec.rb23
-rw-r--r--qa/qa/specs/features/merge_request/rebase_spec.rb2
-rw-r--r--qa/qa/specs/features/merge_request/squash_spec.rb2
-rw-r--r--qa/qa/specs/features/project/activity_spec.rb2
-rw-r--r--qa/qa/specs/features/project/add_deploy_key_spec.rb2
-rw-r--r--qa/qa/specs/features/project/add_secret_variable_spec.rb2
-rw-r--r--qa/qa/specs/features/project/auto_devops_spec.rb9
-rw-r--r--qa/qa/specs/features/project/create_issue_spec.rb2
-rw-r--r--qa/qa/specs/features/project/create_spec.rb2
-rw-r--r--qa/qa/specs/features/project/deploy_key_clone_spec.rb2
-rw-r--r--qa/qa/specs/features/project/fork_project_spec.rb12
-rw-r--r--qa/qa/specs/features/project/import_from_github_spec.rb2
-rw-r--r--qa/qa/specs/features/project/pipelines_spec.rb2
-rw-r--r--qa/qa/specs/features/project/wikis_spec.rb2
-rw-r--r--qa/qa/specs/features/repository/clone_spec.rb2
-rw-r--r--qa/qa/specs/features/repository/protected_branches_spec.rb2
-rw-r--r--qa/qa/specs/features/repository/push_spec.rb2
-rw-r--r--qa/qa/specs/runner.rb8
-rw-r--r--qa/spec/runtime/env_spec.rb25
-rw-r--r--qa/spec/scenario/test/instance/all_spec.rb (renamed from qa/spec/scenario/test/instance_spec.rb)14
-rw-r--r--qa/spec/scenario/test/instance/smoke_spec.rb45
-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/autocomplete_controller_spec.rb13
-rw-r--r--spec/controllers/omniauth_callbacks_controller_spec.rb2
-rw-r--r--spec/controllers/projects/releases_controller_spec.rb2
-rw-r--r--spec/features/admin/admin_runners_spec.rb4
-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/finders/autocomplete/group_finder_spec.rb58
-rw-r--r--spec/finders/autocomplete/move_to_project_finder_spec.rb (renamed from spec/finders/move_to_project_finder_spec.rb)64
-rw-r--r--spec/finders/autocomplete/project_finder_spec.rb55
-rw-r--r--spec/finders/autocomplete/users_finder_spec.rb (renamed from spec/finders/autocomplete_users_finder_spec.rb)2
-rw-r--r--spec/finders/awarded_emoji_finder_spec.rb25
-rw-r--r--spec/finders/user_finder_spec.rb43
-rw-r--r--spec/javascripts/ide/stores/actions/file_spec.js20
-rw-r--r--spec/javascripts/jobs/commit_block_spec.js73
-rw-r--r--spec/javascripts/jobs/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/banzai/filter/spaced_link_filter_spec.rb66
-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/data_builder/build_spec.rb1
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb6
-rw-r--r--spec/lib/gitlab/gitaly_client/diff_spec.rb6
-rw-r--r--spec/lib/gitlab/github_import/bulk_importing_spec.rb12
-rw-r--r--spec/lib/gitlab/github_import/importer/issue_importer_spec.rb22
-rw-r--r--spec/lib/gitlab/github_import/importer/milestones_importer_spec.rb14
-rw-r--r--spec/lib/gitlab/github_import/importer/pull_request_importer_spec.rb10
-rw-r--r--spec/lib/gitlab/import_export/all_models.yml1
-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/award_emoji_spec.rb23
-rw-r--r--spec/models/clusters/applications/prometheus_spec.rb17
-rw-r--r--spec/models/concerns/optionally_search_spec.rb44
-rw-r--r--spec/models/fork_network_member_spec.rb2
-rw-r--r--spec/models/hooks/system_hook_spec.rb4
-rw-r--r--spec/models/merge_request_spec.rb2
-rw-r--r--spec/models/project_group_link_spec.rb2
-rw-r--r--spec/models/project_services/gemnasium_service_spec.rb74
-rw-r--r--spec/models/project_services/jira_service_spec.rb255
-rw-r--r--spec/models/project_spec.rb48
-rw-r--r--spec/models/user_spec.rb140
-rw-r--r--spec/policies/group_policy_spec.rb2
-rw-r--r--spec/requests/api/events_spec.rb48
-rw-r--r--spec/requests/api/project_hooks_spec.rb5
-rw-r--r--spec/requests/api/project_import_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb105
-rw-r--r--spec/rubocop/cop/destroy_all_spec.rb43
-rw-r--r--spec/serializers/move_to_project_entity_spec.rb19
-rw-r--r--spec/serializers/move_to_project_serializer_spec.rb14
-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/todo_service_spec.rb2
-rw-r--r--spec/services/users/destroy_service_spec.rb39
-rw-r--r--spec/spec_helper.rb1
-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/workers/project_destroy_worker_spec.rb7
-rw-r--r--spec/workers/repository_check/single_repository_worker_spec.rb2
-rw-r--r--vendor/licenses.csv1
-rw-r--r--yarn.lock80
268 files changed, 4470 insertions, 1171 deletions
diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index fd02d72b4c2..797a20ef16e 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -739,7 +739,7 @@ karma:
- chrome_debug.log
- coverage-javascript/
-codequality:
+code_quality:
<<: *dedicated-no-docs-no-db-pull-cache-job
image: docker:stable
allow_failure: true
@@ -757,9 +757,13 @@ codequality:
script:
# Extract "MAJOR.MINOR" from CI_SERVER_VERSION and generate "MAJOR-MINOR-stable" for Security Products
- export SP_VERSION=$(echo "$CI_SERVER_VERSION" | sed 's/^\([0-9]*\)\.\([0-9]*\).*/\1-\2-stable/')
- - docker run --env SOURCE_CODE="$PWD" --volume "$PWD":/code --volume /var/run/docker.sock:/var/run/docker.sock "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
+ - docker run
+ --env SOURCE_CODE="$PWD"
+ --volume "$PWD":/code
+ --volume /var/run/docker.sock:/var/run/docker.sock
+ "registry.gitlab.com/gitlab-org/security-products/codequality:$SP_VERSION" /code
artifacts:
- paths: [codeclimate.json]
+ paths: [gl-code-quality-report.json]
expire_in: 1 week
sast:
diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md
index e68e3b9cab0..fb7c0c88629 100644
--- a/CONTRIBUTING.md
+++ b/CONTRIBUTING.md
@@ -64,11 +64,11 @@ As of July 2018, all the documentation for contributing to the GitLab project ha
## Contribute to GitLab
-For a first-time step-by-step guide to the contribution process, see
-["Contributing to GitLab"](https://about.gitlab.com/contributing/).
-
Thank you for your interest in contributing to GitLab. This guide details how
-to contribute to GitLab in a way that is efficient for everyone.
+to contribute to GitLab in a way that is easy for everyone.
+
+For a first-time step-by-step guide to the contribution process, please see
+["Contributing to GitLab"](https://about.gitlab.com/contributing/).
Looking for something to work on? Look for issues with the label [Accepting Merge Requests](#i-want-to-contribute).
@@ -77,10 +77,10 @@ source edition, and GitLab Enterprise Edition (EE) which is our commercial
edition. Throughout this guide you will see references to CE and EE for
abbreviation.
-If you have read this guide and want to know how the GitLab [core team]
+If you want to know how the GitLab [core team]
operates please see [the GitLab contributing process](PROCESS.md).
-- [GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
+[GitLab Inc engineers should refer to the engineering workflow document](https://about.gitlab.com/handbook/engineering/workflow/)
## Security vulnerability disclosure
diff --git a/GITALY_SERVER_VERSION b/GITALY_SERVER_VERSION
index a38b3bd31b1..377d8aca07e 100644
--- a/GITALY_SERVER_VERSION
+++ b/GITALY_SERVER_VERSION
@@ -1 +1 @@
-0.117.0
+0.117.2
diff --git a/Gemfile b/Gemfile
index 5666e6cebc5..bcede83df12 100644
--- a/Gemfile
+++ b/Gemfile
@@ -214,9 +214,6 @@ gem 'jira-ruby', '~> 1.4'
# Flowdock integration
gem 'gitlab-flowdock-git-hook', '~> 1.0.1'
-# Gemnasium integration
-gem 'gemnasium-gitlab-service', '~> 0.2'
-
# Slack integration
gem 'slack-notifier', '~> 1.5.1'
@@ -423,7 +420,7 @@ group :ed25519 do
end
# Gitaly GRPC client
-gem 'gitaly-proto', '~> 0.112.0', require: 'gitaly'
+gem 'gitaly-proto', '~> 0.113.0', require: 'gitaly'
gem 'grpc', '~> 1.11.0'
# Locked until https://github.com/google/protobuf/issues/4210 is closed
diff --git a/Gemfile.lock b/Gemfile.lock
index b33dd75c278..15a105579fb 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -269,8 +269,6 @@ GEM
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
- gemnasium-gitlab-service (0.2.6)
- rugged (~> 0.21)
gemojione (3.3.0)
json
get_process_mem (0.2.0)
@@ -284,7 +282,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.112.0)
+ gitaly-proto (0.113.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -743,7 +741,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)
@@ -1040,12 +1038,11 @@ DEPENDENCIES
font-awesome-rails (~> 4.7)
foreman (~> 0.84.0)
fuubar (~> 2.2.0)
- gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.112.0)
+ gitaly-proto (~> 0.113.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
diff --git a/Gemfile.rails5.lock b/Gemfile.rails5.lock
index af70e2c1939..7803d12c6b4 100644
--- a/Gemfile.rails5.lock
+++ b/Gemfile.rails5.lock
@@ -272,8 +272,6 @@ GEM
fuubar (2.2.0)
rspec-core (~> 3.0)
ruby-progressbar (~> 1.4)
- gemnasium-gitlab-service (0.2.6)
- rugged (~> 0.21)
gemojione (3.3.0)
json
get_process_mem (0.2.0)
@@ -287,7 +285,7 @@ GEM
gettext_i18n_rails (>= 0.7.1)
po_to_json (>= 1.0.0)
rails (>= 3.2.0)
- gitaly-proto (0.112.0)
+ gitaly-proto (0.113.0)
google-protobuf (~> 3.1)
grpc (~> 1.10)
github-linguist (5.3.3)
@@ -1052,12 +1050,11 @@ DEPENDENCIES
font-awesome-rails (~> 4.7)
foreman (~> 0.84.0)
fuubar (~> 2.2.0)
- gemnasium-gitlab-service (~> 0.2)
gemojione (~> 3.3)
gettext (~> 3.2.2)
gettext_i18n_rails (~> 1.8.0)
gettext_i18n_rails_js (~> 1.3)
- gitaly-proto (~> 0.112.0)
+ gitaly-proto (~> 0.113.0)
github-linguist (~> 5.3.3)
gitlab-flowdock-git-hook (~> 1.0.1)
gitlab-gollum-lib (~> 4.2)
@@ -1216,4 +1213,4 @@ DEPENDENCIES
wikicloth (= 0.8.1)
BUNDLED WITH
- 1.16.2
+ 1.16.3
diff --git a/README.md b/README.md
index b6e1cc9a432..335736e53f5 100644
--- a/README.md
+++ b/README.md
@@ -58,7 +58,7 @@ You can access a new installation with the login **`root`** and password **`5ive
## Contributing
-GitLab is an open source project and we are very happy to accept community contributions. Please refer to [CONTRIBUTING.md](/CONTRIBUTING.md) for details.
+GitLab is an open source project and we are very happy to accept community contributions. Please refer to [Contributing to GitLab page](https://about.gitlab.com/contributing/) for more details.
## Licensing
@@ -66,7 +66,7 @@ GitLab Community Edition (CE) is available freely under the MIT Expat license.
All third party components incorporated into the GitLab Software are licensed under the original license provided by the owner of the applicable component.
-All Documentation content that resides under the doc/ directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
+All Documentation content that resides under the `doc/` directory of this repository is licensed under Creative Commons: CC BY-SA 4.0.
## Install a development environment
diff --git a/app/assets/javascripts/ide/components/repo_editor.vue b/app/assets/javascripts/ide/components/repo_editor.vue
index f9badb01535..f55aa843444 100644
--- a/app/assets/javascripts/ide/components/repo_editor.vue
+++ b/app/assets/javascripts/ide/components/repo_editor.vue
@@ -133,7 +133,6 @@ export default {
.then(() =>
this.getRawFileData({
path: this.file.path,
- baseSha: this.currentMergeRequest ? this.currentMergeRequest.baseCommitSha : '',
}),
)
.then(() => {
diff --git a/app/assets/javascripts/ide/stores/actions/file.js b/app/assets/javascripts/ide/stores/actions/file.js
index c9795750d65..28b9d0df201 100644
--- a/app/assets/javascripts/ide/stores/actions/file.js
+++ b/app/assets/javascripts/ide/stores/actions/file.js
@@ -92,7 +92,7 @@ export const setFileMrChange = ({ commit }, { file, mrChange }) => {
commit(types.SET_FILE_MERGE_REQUEST_CHANGE, { file, mrChange });
};
-export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) => {
+export const getRawFileData = ({ state, commit, dispatch, getters }, { path }) => {
const file = state.entries[path];
return new Promise((resolve, reject) => {
service
@@ -100,6 +100,9 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
.then(raw => {
if (!(file.tempFile && !file.prevPath)) commit(types.SET_FILE_RAW_DATA, { file, raw });
if (file.mrChange && file.mrChange.new_file === false) {
+ const baseSha =
+ (getters.currentMergeRequest && getters.currentMergeRequest.baseCommitSha) || '';
+
service
.getBaseRawFileData(file, baseSha)
.then(baseRaw => {
@@ -122,7 +125,7 @@ export const getRawFileData = ({ state, commit, dispatch }, { path, baseSha }) =
action: payload =>
dispatch('getRawFileData', payload).then(() => dispatch('setErrorMessage', null)),
actionText: __('Please try again'),
- actionPayload: { path, baseSha },
+ actionPayload: { path },
});
reject();
});
diff --git a/app/assets/javascripts/importer_status.js b/app/assets/javascripts/importer_status.js
index 0035d809062..eda8cdad908 100644
--- a/app/assets/javascripts/importer_status.js
+++ b/app/assets/javascripts/importer_status.js
@@ -87,7 +87,7 @@ class ImporterStatus {
details = error.response.data.errors;
}
- flash(__(`An error occurred while importing project: ${details}`));
+ flash(sprintf(__('An error occurred while importing project: %{details}'), { details }));
});
}
diff --git a/app/assets/javascripts/jobs/components/commit_block.vue b/app/assets/javascripts/jobs/components/commit_block.vue
new file mode 100644
index 00000000000..7f485295513
--- /dev/null
+++ b/app/assets/javascripts/jobs/components/commit_block.vue
@@ -0,0 +1,64 @@
+<script>
+import ClipboardButton from '~/vue_shared/components/clipboard_button.vue';
+
+export default {
+ components: {
+ ClipboardButton,
+ },
+ props: {
+ pipelineShortSha: {
+ type: String,
+ required: true,
+ },
+ pipelineShaPath: {
+ type: String,
+ required: true,
+ },
+ mergeRequestReference: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ mergeRequestPath: {
+ type: String,
+ required: false,
+ default: null,
+ },
+ gitCommitTitlte: {
+ type: String,
+ required: true,
+ },
+ },
+};
+</script>
+<template>
+ <div class="block">
+ <p>
+ {{ __('Commit') }}
+
+ <a
+ :href="pipelineShaPath"
+ class="js-commit-sha commit-sha link-commit"
+ >
+ {{ pipelineShortSha }}
+ </a>
+
+ <clipboard-button
+ :text="pipelineShortSha"
+ :title="__('Copy commit SHA to clipboard')"
+ />
+
+ <a
+ v-if="mergeRequestPath && mergeRequestReference"
+ :href="mergeRequestPath"
+ class="js-link-commit link-commit"
+ >
+ {{ mergeRequestReference }}
+ </a>
+ </p>
+
+ <p class="build-light-text append-bottom-0">
+ {{ gitCommitTitlte }}
+ </p>
+ </div>
+</template>
diff --git a/app/assets/javascripts/jobs/components/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/monitoring/components/dashboard.vue b/app/assets/javascripts/monitoring/components/dashboard.vue
index 6afaefc56f8..ae96ac3b80c 100644
--- a/app/assets/javascripts/monitoring/components/dashboard.vue
+++ b/app/assets/javascripts/monitoring/components/dashboard.vue
@@ -172,7 +172,7 @@ export default {
<template>
<div
v-if="!showEmptyState"
- class="prometheus-graphs prepend-top-10"
+ class="prometheus-graphs prepend-top-default"
>
<div class="environments d-flex align-items-center">
{{ s__('Metrics|Environment') }}
diff --git a/app/assets/javascripts/pages/admin/cohorts/index.js b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
index 2d5020dbef4..2d5020dbef4 100644
--- a/app/assets/javascripts/pages/admin/cohorts/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/cohorts/index.js
diff --git a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
index 914a9661c27..914a9661c27 100644
--- a/app/assets/javascripts/pages/admin/cohorts/usage_ping.js
+++ b/app/assets/javascripts/pages/instance_statistics/cohorts/usage_ping.js
diff --git a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
index c1056537f90..c1056537f90 100644
--- a/app/assets/javascripts/pages/admin/conversational_development_index/show/index.js
+++ b/app/assets/javascripts/pages/instance_statistics/conversational_development_index/index.js
diff --git a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
index 5e7b8f9698f..63082654101 100644
--- a/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
+++ b/app/assets/javascripts/sidebar/components/lock/edit_form_buttons.vue
@@ -1,4 +1,5 @@
<script>
+import { __ } from '~/locale';
import $ from 'jquery';
import eventHub from '../../event_hub';
@@ -17,7 +18,7 @@ export default {
computed: {
buttonText() {
- return this.isLocked ? this.__('Unlock') : this.__('Lock');
+ return this.isLocked ? __('Unlock') : __('Lock');
},
toggleLock() {
diff --git a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
index 8bbc59f623a..ab7fab7e5ca 100644
--- a/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
+++ b/app/assets/javascripts/sidebar/components/lock/lock_issue_sidebar.vue
@@ -1,5 +1,5 @@
<script>
-import { __ } from '~/locale';
+import { __, sprintf } from '~/locale';
import Flash from '~/flash';
import tooltip from '~/vue_shared/directives/tooltip';
import issuableMixin from '~/vue_shared/mixins/issuable';
@@ -79,11 +79,9 @@ export default {
.then(() => window.location.reload())
.catch(() =>
Flash(
- this.__(
- `Something went wrong trying to change the locked state of this ${
- this.issuableDisplayName
- }`,
- ),
+ sprintf(__('Something went wrong trying to change the locked state of this %{issuableDisplayName}'), {
+ issuableDisplayName: this.issuableDisplayName,
+ }),
),
);
},
diff --git a/app/assets/stylesheets/framework/awards.scss b/app/assets/stylesheets/framework/awards.scss
index 8d11b92cf88..a265e4206f1 100644
--- a/app/assets/stylesheets/framework/awards.scss
+++ b/app/assets/stylesheets/framework/awards.scss
@@ -141,8 +141,8 @@
&:hover,
&:active,
&.is-active {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
box-shadow: none;
outline: 0;
diff --git a/app/assets/stylesheets/framework/common.scss b/app/assets/stylesheets/framework/common.scss
index 48a87ea8616..79ca6e61e9a 100644
--- a/app/assets/stylesheets/framework/common.scss
+++ b/app/assets/stylesheets/framework/common.scss
@@ -71,7 +71,7 @@ pre {
}
&.card.card-body-pre {
- border: 1px solid $well-pre-bg;
+ border: 1px solid $gray-darker;
background: $gray-light;
border-radius: 0;
color: $well-pre-color;
@@ -233,7 +233,7 @@ li.note {
.error-message {
padding: 10px;
- background: $error-bg;
+ background: $red-400;
margin: 0;
color: $white-light;
@@ -244,11 +244,11 @@ li.note {
}
.warning_message {
- border-left: 4px solid $warning-message-border;
- color: $warning-message-color;
+ border-left: 4px solid $orange-200;
+ color: $orange-700;
padding: 10px;
margin-bottom: 10px;
- background: $warning-message-bg;
+ background: $orange-100;
padding-left: 20px;
&.centered {
@@ -348,15 +348,6 @@ img.emoji {
}
}
-.profiler-results {
- top: 73px !important;
-
- .profiler-button,
- .profiler-controls {
- border-color: $profiler-border !important;
- }
-}
-
.dropzone .dz-preview .dz-progress {
border-color: $border-color !important;
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 54882633fea..3bdf5bfc93a 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -112,7 +112,7 @@
&.image_file,
&.video {
- background: $file-image-bg;
+ background: $gray-darker;
text-align: center;
padding: 30px;
@@ -131,7 +131,7 @@
}
&.blob-no-preview {
- background: $blob-bg;
+ background: $gray-darker;
text-shadow: 0 1px 2px $white-light;
padding: 100px 0;
}
@@ -146,7 +146,7 @@
}
tr {
- border-bottom: 1px solid $blame-border;
+ border-bottom: 1px solid $gray-darker;
&:last-child {
border-bottom: 0;
@@ -211,7 +211,7 @@
}
&.logs {
- background: $logs-bg;
+ background: $gray-darker;
max-height: 700px;
overflow-y: auto;
@@ -233,7 +233,7 @@
}
&:hover {
- background: $row-hover;
+ background: $blue-50;
}
}
}
diff --git a/app/assets/stylesheets/framework/images.scss b/app/assets/stylesheets/framework/images.scss
index f878ec1ca91..1e93bf2b751 100644
--- a/app/assets/stylesheets/framework/images.scss
+++ b/app/assets/stylesheets/framework/images.scss
@@ -25,7 +25,7 @@
&.svg-#{$width} {
img,
svg {
- width: #{$width + 'px'};
+ max-width: #{$width + 'px'};
}
}
}
diff --git a/app/assets/stylesheets/framework/issue_box.scss b/app/assets/stylesheets/framework/issue_box.scss
index 86de88729ee..da5f80d9d37 100644
--- a/app/assets/stylesheets/framework/issue_box.scss
+++ b/app/assets/stylesheets/framework/issue_box.scss
@@ -39,7 +39,7 @@
}
&.status-box-expired {
- background-color: $issue-status-expired;
+ background-color: $orange-500;
}
&.status-box-upcoming {
diff --git a/app/assets/stylesheets/framework/lists.scss b/app/assets/stylesheets/framework/lists.scss
index 4b67eab05b3..fdc0454d837 100644
--- a/app/assets/stylesheets/framework/lists.scss
+++ b/app/assets/stylesheets/framework/lists.scss
@@ -21,11 +21,11 @@
}
&.disabled {
- color: $list-text-disabled-color;
+ color: $gl-text-color-tertiary;
}
&:not(.ui-sort-disabled):hover {
- background: $row-hover;
+ background: $blue-50;
}
&.unstyled {
@@ -35,12 +35,12 @@
}
&.warning-row {
- background-color: $list-warning-row-bg;
- border-color: $list-warning-row-border;
- color: $list-warning-row-color;
+ background-color: $orange-100;
+ border-color: $orange-200;
+ color: $orange-700;
&:hover {
- background: $list-warning-row-bg;
+ background: $orange-100;
}
}
@@ -73,7 +73,7 @@
}
.card.card-body-title {
- font-size: $list-font-size;
+ font-size: $gl-font-size;
line-height: 18px;
}
}
@@ -109,8 +109,8 @@ ul.content-list {
li {
border-color: $white-normal;
- font-size: $list-font-size;
- color: $list-text-color;
+ font-size: $gl-font-size;
+ color: $gl-text-color;
&.no-description {
.title {
diff --git a/app/assets/stylesheets/framework/mixins.scss b/app/assets/stylesheets/framework/mixins.scss
index 98bf26a5222..7edb89ce6f3 100644
--- a/app/assets/stylesheets/framework/mixins.scss
+++ b/app/assets/stylesheets/framework/mixins.scss
@@ -50,7 +50,7 @@
@include clearfix;
padding: 10px 0;
- border-bottom: 1px solid $list-border-light;
+ border-bottom: 1px solid $gray-darker;
display: block;
margin: 0;
diff --git a/app/assets/stylesheets/framework/typography.scss b/app/assets/stylesheets/framework/typography.scss
index eccc814b747..5c6110737a4 100644
--- a/app/assets/stylesheets/framework/typography.scss
+++ b/app/assets/stylesheets/framework/typography.scss
@@ -7,7 +7,7 @@
}
a {
- color: $md-link-color;
+ color: $blue-600;
}
img:not(.emoji) {
diff --git a/app/assets/stylesheets/framework/variables.scss b/app/assets/stylesheets/framework/variables.scss
index 866cb88ba5b..96508a71bd8 100644
--- a/app/assets/stylesheets/framework/variables.scss
+++ b/app/assets/stylesheets/framework/variables.scss
@@ -203,21 +203,12 @@ $gl-header-color: #4c4e54;
/*
* Lists
*/
-$list-font-size: $gl-font-size;
-$list-title-color: $gl-text-color;
-$list-text-color: $gl-text-color;
-$list-text-disabled-color: $gl-text-color-tertiary;
-$list-border-light: #eee;
$list-border: rgba(0, 0, 0, 0.05);
$list-text-height: 42px;
-$list-warning-row-bg: $orange-100;
-$list-warning-row-border: $orange-200;
-$list-warning-row-color: $orange-700;
/*
* Markdown
*/
-$md-link-color: $blue-600;
$md-area-border: #ddd;
/*
@@ -249,8 +240,6 @@ $gl-bar-padding: 3px;
/*
* Misc
*/
-$row-hover: $blue-50;
-$row-hover-border: $blue-200;
$progress-color: #c0392b;
$header-height: 40px;
$ide-statusbar-height: 25px;
@@ -258,19 +247,13 @@ $fixed-layout-width: 1280px;
$limited-layout-width: 990px;
$container-text-max-width: 540px;
$gl-avatar-size: 40px;
-$error-exclamation-point: $red-500;
$border-radius-default: 4px;
$border-radius-small: 2px;
$settings-icon-size: 18px;
-$provider-btn-not-active-color: $blue-500;
-$link-underline-blue: $blue-500;
-$active-item-blue: $blue-500;
$layout-link-gray: #7e7c7c;
$btn-side-margin: 10px;
$btn-sm-side-margin: 7px;
$btn-margin-5: 5px;
-$issue-status-expired: $orange-500;
-$issuable-sidebar-color: $gl-text-color-secondary;
$sidebar-block-hover-color: #ebebeb;
$group-path-color: #999;
$namespace-kind-color: #aaa;
@@ -292,7 +275,6 @@ $breadcrumb-min-height: 48px;
* Common component specific colors
*/
$hint-color: #999;
-$well-pre-bg: #eee;
$well-pre-color: #555;
$loading-color: #555;
$update-author-color: #999;
@@ -301,10 +283,6 @@ $user-mention-bg-hover: rgba($blue-500, 0.15);
$time-color: #999;
$project-member-show-color: #aaa;
$gl-promo-color: #aaa;
-$error-bg: $red-400;
-$warning-message-bg: $orange-100;
-$warning-message-border: $orange-200;
-$warning-message-color: $orange-700;
$control-group-descr-color: #666;
$table-permission-x-bg: #d9edf7;
$username-color: #666;
@@ -374,7 +352,7 @@ $dropdown-min-height: 40px;
$dropdown-max-height: 312px;
$dropdown-vertical-offset: 4px;
$dropdown-link-color: #555;
-$dropdown-link-hover-bg: $row-hover;
+$dropdown-link-hover-bg: $blue-50;
$dropdown-empty-row-bg: rgba(#000, 0.04);
$dropdown-border-color: $border-color;
$dropdown-shadow-color: rgba(#000, 0.1);
@@ -592,11 +570,7 @@ $events-body-border: #ddd;
/*
* Files
*/
-$file-image-bg: #eee;
-$blob-bg: #eee;
-$blame-border: #eee;
$blame-line-numbers-border: #ddd;
-$logs-bg: #eee;
$logs-li-color: #888;
$logs-p-color: #333;
@@ -667,7 +641,6 @@ $login-devise-error-color: $red-700;
* Nav
*/
$nav-link-gray: #959494;
-$nav-badge-bg: #eee;
$nav-toggle-gray: #666;
/*
diff --git a/app/assets/stylesheets/pages/boards.scss b/app/assets/stylesheets/pages/boards.scss
index 91f470ca709..69d7de886b4 100644
--- a/app/assets/stylesheets/pages/boards.scss
+++ b/app/assets/stylesheets/pages/boards.scss
@@ -288,7 +288,7 @@
&.is-active,
&.is-active .board-card-assignee:hover a {
- background-color: $row-hover;
+ background-color: $blue-50;
}
.badge {
diff --git a/app/assets/stylesheets/pages/cycle_analytics.scss b/app/assets/stylesheets/pages/cycle_analytics.scss
index bba9f38d3dd..bc4c90711d7 100644
--- a/app/assets/stylesheets/pages/cycle_analytics.scss
+++ b/app/assets/stylesheets/pages/cycle_analytics.scss
@@ -165,7 +165,7 @@
border-right-color: transparent;
border-top-color: $border-color;
border-bottom-color: $border-color;
- box-shadow: inset 2px 0 0 0 $active-item-blue;
+ box-shadow: inset 2px 0 0 0 $blue-500;
.stage-name {
font-weight: $gl-font-weight-bold;
diff --git a/app/assets/stylesheets/pages/diff.scss b/app/assets/stylesheets/pages/diff.scss
index 47778110bae..a999a70693e 100644
--- a/app/assets/stylesheets/pages/diff.scss
+++ b/app/assets/stylesheets/pages/diff.scss
@@ -186,7 +186,7 @@
}
.image {
- background: $file-image-bg;
+ background: $gray-darker;
text-align: center;
padding: 30px;
diff --git a/app/assets/stylesheets/pages/events.scss b/app/assets/stylesheets/pages/events.scss
index f79586b68b9..da0c9b44498 100644
--- a/app/assets/stylesheets/pages/events.scss
+++ b/app/assets/stylesheets/pages/events.scss
@@ -6,7 +6,7 @@
font-size: $gl-font-size;
padding: $gl-padding-top 0 $gl-padding-top 40px;
border-bottom: 1px solid $white-normal;
- color: $list-text-color;
+ color: $gl-text-color;
position: relative;
&.event-inline {
@@ -58,7 +58,7 @@
.event-title {
@include str-truncated(calc(100% - 174px));
font-weight: $gl-font-weight-bold;
- color: $list-text-color;
+ color: $gl-text-color;
}
.event-body {
diff --git a/app/assets/stylesheets/pages/groups.scss b/app/assets/stylesheets/pages/groups.scss
index fa8a0f26b5d..60b4d39bb1a 100644
--- a/app/assets/stylesheets/pages/groups.scss
+++ b/app/assets/stylesheets/pages/groups.scss
@@ -385,8 +385,8 @@
padding: $gl-padding-top;
&:hover {
- border-color: $row-hover-border;
- background-color: $row-hover;
+ border-color: $blue-200;
+ background-color: $blue-50;
cursor: pointer;
}
diff --git a/app/assets/stylesheets/pages/issuable.scss b/app/assets/stylesheets/pages/issuable.scss
index d16a63d009a..6f0f82964c8 100644
--- a/app/assets/stylesheets/pages/issuable.scss
+++ b/app/assets/stylesheets/pages/issuable.scss
@@ -423,10 +423,10 @@
width: 100%;
text-align: center;
margin-bottom: 10px;
- color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
svg {
- fill: $issuable-sidebar-color;
+ fill: $gl-text-color-secondary;
}
&:hover:not(.disabled),
@@ -457,14 +457,14 @@
}
.avatar-counter:hover {
- color: $issuable-sidebar-color;
- border-color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
+ border-color: $gl-text-color-secondary;
}
.btn-clipboard {
border: 0;
background: transparent;
- color: $issuable-sidebar-color;
+ color: $gl-text-color-secondary;
&:hover {
color: $gl-text-color;
@@ -821,7 +821,7 @@
svg {
width: 16px;
height: 16px;
- fill: $issuable-sidebar-color;
+ fill: $gl-text-color-secondary;
}
&:hover svg {
diff --git a/app/assets/stylesheets/pages/notes.scss b/app/assets/stylesheets/pages/notes.scss
index 8d28daac750..2e1b2126887 100644
--- a/app/assets/stylesheets/pages/notes.scss
+++ b/app/assets/stylesheets/pages/notes.scss
@@ -141,9 +141,6 @@ ul.notes {
}
.note-body {
- overflow-x: auto;
- overflow-y: hidden;
-
.note-text {
@include md-typography;
// Reset ul style types since we're nested inside a ul already
diff --git a/app/assets/stylesheets/pages/profile.scss b/app/assets/stylesheets/pages/profile.scss
index b45e305897c..17f34319050 100644
--- a/app/assets/stylesheets/pages/profile.scss
+++ b/app/assets/stylesheets/pages/profile.scss
@@ -20,7 +20,7 @@
.account-btn-link,
.profile-settings-sidebar a,
.settings-sidebar a {
- color: $md-link-color;
+ color: $blue-600;
}
.private-tokens-reset div.reset-action:not(:first-child) {
@@ -137,7 +137,7 @@
.profile-settings-content {
a {
- color: $md-link-color;
+ color: $blue-600;
}
}
@@ -170,7 +170,7 @@
background-color: $gray-light;
&.not-active {
- color: $provider-btn-not-active-color;
+ color: $blue-500;
}
}
diff --git a/app/assets/stylesheets/pages/projects.scss b/app/assets/stylesheets/pages/projects.scss
index fffb440027c..c11916454c8 100644
--- a/app/assets/stylesheets/pages/projects.scss
+++ b/app/assets/stylesheets/pages/projects.scss
@@ -449,8 +449,8 @@
&:hover:not(.disabled),
&.forked {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
}
.avatar-container,
@@ -894,7 +894,7 @@ pre.light-well {
.cannot-be-merged,
.cannot-be-merged:hover {
- color: $error-exclamation-point;
+ color: $red-500;
margin-top: 2px;
}
diff --git a/app/assets/stylesheets/pages/settings.scss b/app/assets/stylesheets/pages/settings.scss
index fb03970f64f..5aa4cdec9c3 100644
--- a/app/assets/stylesheets/pages/settings.scss
+++ b/app/assets/stylesheets/pages/settings.scss
@@ -113,9 +113,9 @@
.settings-message {
padding: 5px;
line-height: 1.3;
- color: $warning-message-color;
- background-color: $warning-message-bg;
- border: 1px solid $warning-message-border;
+ color: $orange-700;
+ background-color: $orange-100;
+ border: 1px solid $orange-200;
border-radius: $border-radius-base;
}
@@ -303,7 +303,7 @@
}
.mirror-error-badge {
- background-color: $error-bg;
+ background-color: $red-400;
border-radius: $border-radius-default;
color: $white-light;
}
diff --git a/app/assets/stylesheets/pages/todos.scss b/app/assets/stylesheets/pages/todos.scss
index 010a2c05a1c..5d3b7b21ce4 100644
--- a/app/assets/stylesheets/pages/todos.scss
+++ b/app/assets/stylesheets/pages/todos.scss
@@ -12,8 +12,8 @@
flex-direction: row;
&:hover {
- background-color: $row-hover;
- border-color: $row-hover-border;
+ background-color: $blue-50;
+ border-color: $blue-200;
cursor: pointer;
}
@@ -22,7 +22,7 @@
border-bottom: 1px solid transparent;
&:hover {
- border-color: $row-hover-border;
+ border-color: $blue-200;
}
}
diff --git a/app/assets/stylesheets/pages/tree.scss b/app/assets/stylesheets/pages/tree.scss
index 1cc26d40ba9..dc5ca78ff58 100644
--- a/app/assets/stylesheets/pages/tree.scss
+++ b/app/assets/stylesheets/pages/tree.scss
@@ -111,9 +111,9 @@
&:hover:not(.tree-truncated-warning) {
td {
- background-color: $row-hover;
- border-top: 1px solid $row-hover-border;
- border-bottom: 1px solid $row-hover-border;
+ background-color: $blue-50;
+ border-top: 1px solid $blue-200;
+ border-bottom: 1px solid $blue-200;
cursor: pointer;
}
}
@@ -229,7 +229,7 @@
.upload-link {
font-weight: $gl-font-weight-normal;
- color: $md-link-color;
+ color: $blue-600;
}
.repo-charts {
diff --git a/app/controllers/autocomplete_controller.rb b/app/controllers/autocomplete_controller.rb
index 86bade49ec9..9e30b982b06 100644
--- a/app/controllers/autocomplete_controller.rb
+++ b/app/controllers/autocomplete_controller.rb
@@ -1,67 +1,39 @@
class AutocompleteController < ApplicationController
- AWARD_EMOJI_MAX = 100
-
skip_before_action :authenticate_user!, only: [:users, :award_emojis]
- before_action :load_project, only: [:users]
- before_action :load_group, only: [:users]
def users
- @users = AutocompleteUsersFinder.new(params: params, current_user: current_user, project: @project, group: @group).execute
-
- render json: UserSerializer.new.represent(@users)
- end
-
- def user
- @user = User.find(params[:id])
- render json: UserSerializer.new.represent(@user)
- end
-
- def projects
- project = Project.find_by_id(params[:project_id])
- projects = projects_finder.execute(project, search: params[:search], offset_id: params[:offset_id])
+ project = Autocomplete::ProjectFinder
+ .new(current_user, params)
+ .execute
- render json: projects.to_json(only: [:id, :name_with_namespace], methods: :name_with_namespace)
- end
+ group = Autocomplete::GroupFinder
+ .new(current_user, project, params)
+ .execute
- def award_emojis
- emoji_with_count = AwardEmoji
- .limit(AWARD_EMOJI_MAX)
- .where(user: current_user)
- .group(:name)
- .order('count_all DESC, name ASC')
- .count
+ users = Autocomplete::UsersFinder
+ .new(params: params, current_user: current_user, project: project, group: group)
+ .execute
- # Transform from hash to array to guarantee json order
- # e.g. { 'thumbsup' => 2, 'thumbsdown' = 1 }
- # => [{ name: 'thumbsup' }, { name: 'thumbsdown' }]
- render json: emoji_with_count.map { |k, v| { name: k } }
+ render json: UserSerializer.new.represent(users)
end
- private
-
- def load_group
- @group ||= begin
- if @project.blank? && params[:group_id].present?
- group = Group.find(params[:group_id])
- return render_404 unless can?(current_user, :read_group, group)
+ def user
+ user = UserFinder.new(params).execute!
- group
- end
- end
+ render json: UserSerializer.new.represent(user)
end
- def load_project
- @project ||= begin
- if params[:project_id].present?
- project = Project.find(params[:project_id])
- return render_404 unless can?(current_user, :read_project, project)
+ # Displays projects to use for the dropdown when moving a resource from one
+ # project to another.
+ def projects
+ projects = Autocomplete::MoveToProjectFinder
+ .new(current_user, params)
+ .execute
- project
- end
- end
+ render json: MoveToProjectSerializer.new.represent(projects)
end
- def projects_finder
- MoveToProjectFinder.new(current_user)
+ def award_emojis
+ render json: AwardedEmojiFinder.new(current_user).execute
end
end
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/autocomplete/group_finder.rb b/app/finders/autocomplete/group_finder.rb
new file mode 100644
index 00000000000..dd97ac4c817
--- /dev/null
+++ b/app/finders/autocomplete/group_finder.rb
@@ -0,0 +1,37 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ # Finder for retrieving a group to use for autocomplete data sources.
+ class GroupFinder
+ attr_reader :current_user, :project, :group_id
+
+ # current_user - The currently logged in user, if any.
+ # project - The Project (if any) to use for the autocomplete data sources.
+ # params - A Hash containing parameters to use for finding the project.
+ #
+ # The following parameters are supported:
+ #
+ # * group_id: The ID of the group to find.
+ def initialize(current_user = nil, project = nil, params = {})
+ @current_user = current_user
+ @project = project
+ @group_id = params[:group_id]
+ end
+
+ # Attempts to find a Group based on the current group ID.
+ def execute
+ return unless project.blank? && group_id.present?
+
+ group = Group.find(group_id)
+
+ # This removes the need for using `return render_404` and similar patterns
+ # in controllers that use this finder.
+ unless Ability.allowed?(current_user, :read_group, group)
+ raise ActiveRecord::RecordNotFound
+ .new("Could not find a Group with ID #{group_id}")
+ end
+
+ group
+ end
+ end
+end
diff --git a/app/finders/autocomplete/move_to_project_finder.rb b/app/finders/autocomplete/move_to_project_finder.rb
new file mode 100644
index 00000000000..edaf74c5f92
--- /dev/null
+++ b/app/finders/autocomplete/move_to_project_finder.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ # Finder that retrieves a list of projects that an issue can be moved to.
+ class MoveToProjectFinder
+ attr_reader :current_user, :search, :project_id, :offset_id
+
+ # current_user - The User object of the user that wants to view the list of
+ # projects.
+ #
+ # params - A Hash containing additional parameters to set.
+ #
+ # The following parameters can be set (as Symbols):
+ #
+ # * search: An optional search query to apply to the list of projects.
+ # * project_id: The ID of a project to exclude from the returned relation.
+ # * offset_id: The ID of a project to use for pagination. When given, only
+ # projects with a lower ID are included in the list.
+ def initialize(current_user, params = {})
+ @current_user = current_user
+ @search = params[:search]
+ @project_id = params[:project_id]
+ @offset_id = params[:offset_id]
+ end
+
+ def execute
+ current_user
+ .projects_where_can_admin_issues
+ .optionally_search(search)
+ .excluding_project(project_id)
+ .paginate_in_descending_order_using_id(before: offset_id)
+ .eager_load_namespace_and_owner
+ end
+ end
+end
diff --git a/app/finders/autocomplete/project_finder.rb b/app/finders/autocomplete/project_finder.rb
new file mode 100644
index 00000000000..3a4696f4c2e
--- /dev/null
+++ b/app/finders/autocomplete/project_finder.rb
@@ -0,0 +1,35 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ # Finder for retrieving a project to use for autocomplete data sources.
+ class ProjectFinder
+ attr_reader :current_user, :project_id
+
+ # current_user - The currently logged in user, if any.
+ # params - A Hash containing parameters to use for finding the project.
+ #
+ # The following parameters are supported:
+ #
+ # * project_id: The ID of the project to find.
+ def initialize(current_user = nil, params = {})
+ @current_user = current_user
+ @project_id = params[:project_id]
+ end
+
+ # Attempts to find a Project based on the current project ID.
+ def execute
+ return if project_id.blank?
+
+ project = Project.find(project_id)
+
+ # This removes the need for using `return render_404` and similar patterns
+ # in controllers that use this finder.
+ unless Ability.allowed?(current_user, :read_project, project)
+ raise ActiveRecord::RecordNotFound
+ .new("Could not find a Project with ID #{project_id}")
+ end
+
+ project
+ end
+ end
+end
diff --git a/app/finders/autocomplete/users_finder.rb b/app/finders/autocomplete/users_finder.rb
new file mode 100644
index 00000000000..b2557469079
--- /dev/null
+++ b/app/finders/autocomplete/users_finder.rb
@@ -0,0 +1,85 @@
+# frozen_string_literal: true
+
+module Autocomplete
+ class UsersFinder
+ # The number of users to display in the results is hardcoded to 20, and
+ # pagination is not supported. This ensures that performance remains
+ # consistent and removes the need for implementing keyset pagination to
+ # ensure good performance.
+ LIMIT = 20
+
+ attr_reader :current_user, :project, :group, :search, :skip_users,
+ :author_id, :todo_filter, :todo_state_filter,
+ :filter_by_current_user
+
+ def initialize(params:, current_user:, project:, group:)
+ @current_user = current_user
+ @project = project
+ @group = group
+ @search = params[:search]
+ @skip_users = params[:skip_users]
+ @author_id = params[:author_id]
+ @todo_filter = params[:todo_filter]
+ @todo_state_filter = params[:todo_state_filter]
+ @filter_by_current_user = params[:current_user]
+ end
+
+ def execute
+ items = limited_users
+
+ if search.blank?
+ # Include current user if available to filter by "Me"
+ items.unshift(current_user) if prepend_current_user?
+
+ if prepend_author? && (author = User.find_by_id(author_id))
+ items.unshift(author)
+ end
+ end
+
+ items.uniq
+ end
+
+ private
+
+ # Returns the users based on the input parameters, as an Array.
+ #
+ # This method is separate so it is easier to extend in EE.
+ def limited_users
+ # When changing the order of these method calls, make sure that
+ # reorder_by_name() is called _before_ optionally_search(), otherwise
+ # reorder_by_name will break the ORDER BY applied in optionally_search().
+ find_users
+ .active
+ .reorder_by_name
+ .optionally_search(search)
+ .where_not_in(skip_users)
+ .limit_to_todo_authors(
+ user: current_user,
+ with_todos: todo_filter,
+ todo_state: todo_state_filter
+ )
+ .limit(LIMIT)
+ .to_a
+ end
+
+ def prepend_current_user?
+ filter_by_current_user.present? && current_user
+ end
+
+ def prepend_author?
+ author_id.present? && current_user
+ end
+
+ def find_users
+ if project
+ project.authorized_users.union_with_user(author_id)
+ elsif group
+ group.users_with_parents
+ elsif current_user
+ User.all
+ else
+ User.none
+ end
+ end
+ end
+end
diff --git a/app/finders/autocomplete_users_finder.rb b/app/finders/autocomplete_users_finder.rb
deleted file mode 100644
index e8a03947f59..00000000000
--- a/app/finders/autocomplete_users_finder.rb
+++ /dev/null
@@ -1,68 +0,0 @@
-class AutocompleteUsersFinder
- # The number of users to display in the results is hardcoded to 20, and
- # pagination is not supported. This ensures that performance remains
- # consistent and removes the need for implementing keyset pagination to ensure
- # good performance.
- LIMIT = 20
-
- attr_reader :current_user, :project, :group, :search, :skip_users,
- :author_id, :params
-
- def initialize(params:, current_user:, project:, group:)
- @current_user = current_user
- @project = project
- @group = group
- @search = params[:search]
- @skip_users = params[:skip_users]
- @author_id = params[:author_id]
- @params = params
- end
-
- def execute
- items = find_users
- items = items.active
- items = items.reorder(:name)
- items = items.search(search) if search.present?
- items = items.where.not(id: skip_users) if skip_users.present?
- items = items.limit(LIMIT)
-
- if params[:todo_filter].present? && current_user
- items = items.todo_authors(current_user.id, params[:todo_state_filter])
- end
-
- if search.blank?
- # Include current user if available to filter by "Me"
- if params[:current_user].present? && current_user
- items = [current_user, *items].uniq
- end
-
- if author_id.present? && current_user
- author = User.find_by_id(author_id)
- items = [author, *items].uniq if author
- end
- end
-
- items
- end
-
- private
-
- def find_users
- return users_from_project if project
- return group.users_with_parents if group
- return User.all if current_user
-
- User.none
- end
-
- def users_from_project
- if author_id.present?
- union = Gitlab::SQL::Union
- .new([project.authorized_users, User.where(id: author_id)])
-
- User.from("(#{union.to_sql}) #{User.table_name}")
- else
- project.authorized_users
- end
- end
-end
diff --git a/app/finders/awarded_emoji_finder.rb b/app/finders/awarded_emoji_finder.rb
new file mode 100644
index 00000000000..f0cc17f3b26
--- /dev/null
+++ b/app/finders/awarded_emoji_finder.rb
@@ -0,0 +1,21 @@
+# frozen_string_literal: true
+
+# Class for retrieving information about emoji awarded _by_ a particular user.
+class AwardedEmojiFinder
+ attr_reader :current_user
+
+ # current_user - The User to generate the data for.
+ def initialize(current_user = nil)
+ @current_user = current_user
+ end
+
+ def execute
+ return [] unless current_user
+
+ # We want the resulting data set to be an Array containing the emoji names
+ # in descending order, based on how often they were awarded.
+ AwardEmoji
+ .award_counts_for_user(current_user)
+ .map { |name, _| { name: name } }
+ end
+end
diff --git a/app/finders/move_to_project_finder.rb b/app/finders/move_to_project_finder.rb
deleted file mode 100644
index 038d5565a1e..00000000000
--- a/app/finders/move_to_project_finder.rb
+++ /dev/null
@@ -1,21 +0,0 @@
-class MoveToProjectFinder
- PAGE_SIZE = 50
-
- def initialize(user)
- @user = user
- end
-
- def execute(from_project, search: nil, offset_id: nil)
- projects = @user.projects_where_can_admin_issues
- projects = projects.search(search) if search.present?
- projects = projects.excluding_project(from_project)
- projects = projects.order_id_desc
-
- # infinite scroll using offset
- projects = projects.where('projects.id < ?', offset_id) if offset_id.present?
- projects = projects.limit(PAGE_SIZE)
-
- # to ask for Project#name_with_namespace
- projects.includes(namespace: :owner)
- end
-end
diff --git a/app/finders/user_finder.rb b/app/finders/user_finder.rb
new file mode 100644
index 00000000000..484a93c9873
--- /dev/null
+++ b/app/finders/user_finder.rb
@@ -0,0 +1,26 @@
+# frozen_string_literal: true
+
+# A simple finding for obtaining a single User.
+#
+# While using `User.find_by` directly is straightforward, it can lead to a lot
+# of code duplication. Sometimes we just want to find a user by an ID, other
+# times we may want to exclude blocked user. By using this finder (and extending
+# it whenever necessary) we can keep this logic in one place.
+class UserFinder
+ attr_reader :params
+
+ def initialize(params)
+ @params = params
+ end
+
+ # Tries to find a User, returning nil if none could be found.
+ def execute
+ User.find_by(id: params[:id])
+ end
+
+ # Tries to find a User, raising a `ActiveRecord::RecordNotFound` if it could
+ # not be found.
+ def execute!
+ User.find(params[:id])
+ end
+end
diff --git a/app/models/award_emoji.rb b/app/models/award_emoji.rb
index 99c7866d636..ddc516ccb60 100644
--- a/app/models/award_emoji.rb
+++ b/app/models/award_emoji.rb
@@ -28,6 +28,23 @@ class AwardEmoji < ActiveRecord::Base
.where('name IN (?) AND awardable_type = ? AND awardable_id IN (?)', [DOWNVOTE_NAME, UPVOTE_NAME], type, ids)
.group('name', 'awardable_id')
end
+
+ # Returns the top 100 emoji awarded by the given user.
+ #
+ # The returned value is a Hash mapping emoji names to the number of times
+ # they were awarded:
+ #
+ # { 'thumbsup' => 2, 'thumbsdown' => 1 }
+ #
+ # user - The User to get the awards for.
+ # limt - The maximum number of emoji to return.
+ def award_counts_for_user(user, limit = 100)
+ limit(limit)
+ .where(user: user)
+ .group(:name)
+ .order('count_all DESC, name ASC')
+ .count
+ end
end
def downvote?
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/concerns/optionally_search.rb b/app/models/concerns/optionally_search.rb
new file mode 100644
index 00000000000..dec97b7dee8
--- /dev/null
+++ b/app/models/concerns/optionally_search.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+module OptionallySearch
+ extend ActiveSupport::Concern
+
+ module ClassMethods
+ def search(*)
+ raise(
+ NotImplementedError,
+ 'Your model must implement the "search" class method'
+ )
+ end
+
+ # Optionally limits a result set to those matching the given search query.
+ def optionally_search(query = nil)
+ query.present? ? search(query) : all
+ end
+ end
+end
diff --git a/app/models/internal_id.rb b/app/models/internal_id.rb
index 4eb211eff61..e7168d49db9 100644
--- a/app/models/internal_id.rb
+++ b/app/models/internal_id.rb
@@ -111,7 +111,7 @@ class InternalId < ActiveRecord::Base
# Generates next internal id and returns it
def generate
- subject.transaction do
+ InternalId.transaction do
# Create a record in internal_ids if one does not yet exist
# and increment its last value
#
@@ -125,7 +125,7 @@ class InternalId < ActiveRecord::Base
#
# Note this will acquire a ROW SHARE lock on the InternalId record
def track_greatest(new_value)
- subject.transaction do
+ InternalId.transaction do
(lookup || create_record).track_greatest_and_save!(new_value)
end
end
@@ -148,7 +148,7 @@ class InternalId < ActiveRecord::Base
# violation. We can safely roll-back the nested transaction and perform
# a lookup instead to retrieve the record.
def create_record
- subject.transaction(requires_new: true) do
+ InternalId.transaction(requires_new: true) do
InternalId.create!(
**scope,
usage: usage_value,
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
index 2a1a4ef48b7..97bf5d611c2 100644
--- a/app/models/lfs_object.rb
+++ b/app/models/lfs_object.rb
@@ -29,11 +29,13 @@ class LfsObject < ActiveRecord::Base
[nil, LfsObjectUploader::Store::LOCAL].include?(self.file_store)
end
+ # rubocop: disable DestroyAll
def self.destroy_unreferenced
joins("LEFT JOIN lfs_objects_projects ON lfs_objects_projects.lfs_object_id = #{table_name}.id")
.where(lfs_objects_projects: { id: nil })
.destroy_all
end
+ # rubocop: enable DestroyAll
def self.calculate_oid(path)
Digest::SHA256.file(path).hexdigest
diff --git a/app/models/project.rb b/app/models/project.rb
index 94c1d60f071..8f631d7f0ed 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -28,6 +28,7 @@ class Project < ActiveRecord::Base
include WithUploads
include BatchDestroyDependentAssociations
include FeatureGate
+ include OptionallySearch
extend Gitlab::Cache::RequestCache
extend Gitlab::ConfigHelper
@@ -140,7 +141,6 @@ class Project < ActiveRecord::Base
has_one :flowdock_service
has_one :assembla_service
has_one :asana_service
- has_one :gemnasium_service
has_one :mattermost_slash_commands_service
has_one :mattermost_service
has_one :slack_slash_commands_service
@@ -384,6 +384,26 @@ class Project < ActiveRecord::Base
only_integer: true,
message: 'needs to be beetween 10 minutes and 1 month' }
+ # Paginates a collection using a `WHERE id < ?` condition.
+ #
+ # before - A project ID to use for filtering out projects with an equal or
+ # greater ID. If no ID is given, all projects are included.
+ #
+ # limit - The maximum number of rows to include.
+ def self.paginate_in_descending_order_using_id(
+ before: nil,
+ limit: Kaminari.config.default_per_page
+ )
+ relation = order_id_desc.limit(limit)
+ relation = relation.where('projects.id < ?', before) if before
+
+ relation
+ end
+
+ def self.eager_load_namespace_and_owner
+ includes(namespace: :owner)
+ end
+
# Returns a collection of projects that is either public or visible to the
# logged in user.
def self.public_or_visible_to_user(user = nil)
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
deleted file mode 100644
index 67a92c441b1..00000000000
--- a/app/models/project_services/gemnasium_service.rb
+++ /dev/null
@@ -1,62 +0,0 @@
-# frozen_string_literal: true
-
-require "gemnasium/gitlab_service"
-
-class GemnasiumService < Service
- prop_accessor :token, :api_key
- validates :token, :api_key, presence: true, if: :activated?
- validate :deprecation_validation
-
- def title
- 'Gemnasium'
- end
-
- def description
- 'Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.'
- end
-
- def self.to_param
- 'gemnasium'
- end
-
- def fields
- [
- { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ', required: true },
- { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com', required: true }
- ]
- end
-
- def self.supported_events
- %w(push)
- end
-
- def deprecated?
- true
- end
-
- def deprecation_message
- "Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available."
- end
-
- def deprecation_validation
- errors[:base] << deprecation_message
- end
-
- def execute(data)
- return unless supported_events.include?(data[:object_kind])
-
- # Gitaly: this class will be removed https://gitlab.com/gitlab-org/gitlab-ee/issues/6010
- repo_path = Gitlab::GitalyClient::StorageSettings.allow_disk_access do
- project.repository.path_to_repo
- end
-
- Gemnasium::GitlabService.execute(
- ref: data[:ref],
- before: data[:before],
- after: data[:after],
- token: token,
- api_key: api_key,
- repo: repo_path
- )
- 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..a6ba90794d6 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -19,6 +19,7 @@ class User < ActiveRecord::Base
include BulkMemberAccessLoad
include BlocksJsonSerialization
include WithUploads
+ include OptionallySearch
DEFAULT_NOTIFICATION_LEVEL = :participating
@@ -253,11 +254,41 @@ class User < ActiveRecord::Base
scope :external, -> { where(external: true) }
scope :active, -> { with_state(:active).non_internal }
scope :without_projects, -> { joins('LEFT JOIN project_authorizations ON users.id = project_authorizations.user_id').where(project_authorizations: { user_id: nil }) }
- scope :todo_authors, ->(user_id, state) { where(id: Todo.where(user_id: user_id, state: state).select(:author_id)) }
scope :order_recent_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'DESC')) }
scope :order_oldest_sign_in, -> { reorder(Gitlab::Database.nulls_last_order('current_sign_in_at', 'ASC')) }
scope :confirmed, -> { where.not(confirmed_at: nil) }
+ # Limits the users to those that have TODOs, optionally in the given state.
+ #
+ # user - The user to get the todos for.
+ #
+ # with_todos - If we should limit the result set to users that are the
+ # authors of todos.
+ #
+ # todo_state - An optional state to require the todos to be in.
+ def self.limit_to_todo_authors(user: nil, with_todos: false, todo_state: nil)
+ if user && with_todos
+ where(id: Todo.where(user: user, state: todo_state).select(:author_id))
+ else
+ all
+ end
+ end
+
+ # Returns a relation that optionally includes the given user.
+ #
+ # user_id - The ID of the user to include.
+ def self.union_with_user(user_id = nil)
+ if user_id.present?
+ union = Gitlab::SQL::Union.new([all, User.unscoped.where(id: user_id)])
+
+ # We use "unscoped" here so that any inner conditions are not repeated for
+ # the outer query, which would be redundant.
+ User.unscoped.from("(#{union.to_sql}) #{User.table_name}")
+ else
+ all
+ end
+ end
+
def self.with_two_factor_indistinct
joins("LEFT OUTER JOIN u2f_registrations AS u2f ON u2f.user_id = users.id")
.where("u2f.id IS NOT NULL OR users.otp_required_for_login = ?", true)
@@ -365,6 +396,18 @@ class User < ActiveRecord::Base
).reorder(order % { query: ActiveRecord::Base.connection.quote(query) }, :name)
end
+ # Limits the result set to users _not_ in the given query/list of IDs.
+ #
+ # users - The list of users to ignore. This can be an
+ # `ActiveRecord::Relation`, or an Array.
+ def where_not_in(users = nil)
+ users ? where.not(id: users) : all
+ end
+
+ def reorder_by_name
+ reorder(:name)
+ end
+
# searches user by given pattern
# it compares name, email, username fields and user's secondary emails with given pattern
# This method uses ILIKE on PostgreSQL and LIKE on MySQL.
@@ -516,7 +559,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/serializers/move_to_project_entity.rb b/app/serializers/move_to_project_entity.rb
new file mode 100644
index 00000000000..dac1124b0b3
--- /dev/null
+++ b/app/serializers/move_to_project_entity.rb
@@ -0,0 +1,6 @@
+# frozen_string_literal: true
+
+class MoveToProjectEntity < Grape::Entity
+ expose :id
+ expose :name_with_namespace
+end
diff --git a/app/serializers/move_to_project_serializer.rb b/app/serializers/move_to_project_serializer.rb
new file mode 100644
index 00000000000..6a59317505c
--- /dev/null
+++ b/app/serializers/move_to_project_serializer.rb
@@ -0,0 +1,5 @@
+# frozen_string_literal: true
+
+class MoveToProjectSerializer < BaseSerializer
+ entity MoveToProjectEntity
+end
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/protected_branches/legacy_api_update_service.rb b/app/services/protected_branches/legacy_api_update_service.rb
index 1f6bbe72f85..da8bf2ce02a 100644
--- a/app/services/protected_branches/legacy_api_update_service.rb
+++ b/app/services/protected_branches/legacy_api_update_service.rb
@@ -38,11 +38,11 @@ module ProtectedBranches
def delete_redundant_access_levels
unless @developers_can_merge.nil?
- @protected_branch.merge_access_levels.destroy_all
+ @protected_branch.merge_access_levels.destroy_all # rubocop: disable DestroyAll
end
unless @developers_can_push.nil?
- @protected_branch.push_access_levels.destroy_all
+ @protected_branch.push_access_levels.destroy_all # rubocop: disable DestroyAll
end
end
end
diff --git a/app/services/users/destroy_service.rb b/app/services/users/destroy_service.rb
index 4bc78b5b64e..73fa6089945 100644
--- a/app/services/users/destroy_service.rb
+++ b/app/services/users/destroy_service.rb
@@ -2,6 +2,8 @@
module Users
class DestroyService
+ DestroyError = Class.new(StandardError)
+
attr_accessor :current_user
def initialize(current_user)
@@ -46,9 +48,8 @@ module Users
namespace.prepare_for_destroy
user.personal_projects.each do |project|
- # Skip repository removal because we remove directory with namespace
- # that contain all this repositories
- ::Projects::DestroyService.new(project, current_user, skip_repo: project.legacy_storage?).execute
+ success = ::Projects::DestroyService.new(project, current_user).execute
+ raise DestroyError, "Project #{project.id} can't be deleted" unless success
end
yield(user) if block_given?
diff --git a/app/views/admin/application_settings/_ci_cd.html.haml b/app/views/admin/application_settings/_ci_cd.html.haml
index 5037017e38a..97be658cd34 100644
--- a/app/views/admin/application_settings/_ci_cd.html.haml
+++ b/app/views/admin/application_settings/_ci_cd.html.haml
@@ -38,6 +38,8 @@
.form-text.text-muted
Set the default expiration time for each job's artifacts.
0 for unlimited.
+ The default unit is in seconds, but you can define an alternative. For example:
+ <code>4 mins 2 sec</code>, <code>2h42min</code>.
= link_to icon('question-circle'), help_page_path('user/admin_area/settings/continuous_integration', anchor: 'default-artifacts-expiration')
= f.submit 'Save changes', class: "btn btn-success"
diff --git a/app/views/admin/runners/index.html.haml b/app/views/admin/runners/index.html.haml
index 8dfd176f1b7..9280ff4d478 100644
--- a/app/views/admin/runners/index.html.haml
+++ b/app/views/admin/runners/index.html.haml
@@ -49,7 +49,7 @@
= submit_tag 'Search', class: 'btn'
.float-right.light
- Runners with last contact more than a minute ago: #{@active_runners_cnt}
+ Runners currently online: #{@active_runners_cnt}
%br
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/projects/environments/metrics.html.haml b/app/views/projects/environments/metrics.html.haml
index af86b8e8e67..4222963a754 100644
--- a/app/views/projects/environments/metrics.html.haml
+++ b/app/views/projects/environments/metrics.html.haml
@@ -2,11 +2,4 @@
- page_title "Metrics for environment", @environment.name
.prometheus-container{ class: container_class }
- .top-area
- .row
- .col-sm-6
- %h3
- Environment:
- = link_to @environment.name, environment_path(@environment)
-
#prometheus-graphs{ data: metrics_data(@project, @environment) }
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/runners/_specific_runners.html.haml b/app/views/projects/runners/_specific_runners.html.haml
index 6c11ce3b394..314af44490e 100644
--- a/app/views/projects/runners/_specific_runners.html.haml
+++ b/app/views/projects/runners/_specific_runners.html.haml
@@ -13,4 +13,4 @@
%h4.underlined-title Available specific runners
%ul.bordered-list.available-specific-runners
= render partial: 'projects/runners/runner', collection: @assignable_runners, as: :runner
- = paginate @assignable_runners, theme: "gitlab"
+ = paginate @assignable_runners, theme: "gitlab", :params => { :anchor => '#js-runners-settings' }
diff --git a/app/workers/remove_expired_group_links_worker.rb b/app/workers/remove_expired_group_links_worker.rb
index 6b8b972a440..25128caf72f 100644
--- a/app/workers/remove_expired_group_links_worker.rb
+++ b/app/workers/remove_expired_group_links_worker.rb
@@ -5,6 +5,6 @@ class RemoveExpiredGroupLinksWorker
include CronjobQueue
def perform
- ProjectGroupLink.expired.destroy_all
+ ProjectGroupLink.expired.destroy_all # rubocop: disable DestroyAll
end
end
diff --git a/app/workers/remove_old_web_hook_logs_worker.rb b/app/workers/remove_old_web_hook_logs_worker.rb
index 17140ac4450..0f486f8991d 100644
--- a/app/workers/remove_old_web_hook_logs_worker.rb
+++ b/app/workers/remove_old_web_hook_logs_worker.rb
@@ -6,7 +6,9 @@ class RemoveOldWebHookLogsWorker
WEB_HOOK_LOG_LIFETIME = 2.days
+ # rubocop: disable DestroyAll
def perform
WebHookLog.destroy_all(['created_at < ?', Time.now - WEB_HOOK_LOG_LIFETIME])
end
+ # rubocop: enable DestroyAll
end
diff --git a/changelogs/unreleased/42754-runners-pagination.yml b/changelogs/unreleased/42754-runners-pagination.yml
new file mode 100644
index 00000000000..8e77b857538
--- /dev/null
+++ b/changelogs/unreleased/42754-runners-pagination.yml
@@ -0,0 +1,5 @@
+---
+title: Does not collapse runners section when using pagination
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml
new file mode 100644
index 00000000000..3f85f75bef4
--- /dev/null
+++ b/changelogs/unreleased/47845-propagate_failure_reason-to-job-webhook.yml
@@ -0,0 +1,5 @@
+---
+title: "#47845 Add failure_reason to job webhook"
+merge_request: 21143
+author: matemaciek
+type: added
diff --git a/changelogs/unreleased/48145-illustration.yml b/changelogs/unreleased/48145-illustration.yml
new file mode 100644
index 00000000000..7d84075c2b3
--- /dev/null
+++ b/changelogs/unreleased/48145-illustration.yml
@@ -0,0 +1,5 @@
+---
+title: Fixes SVGs for empty states in job page overflowing on mobile
+merge_request:
+author:
+type: fixed
diff --git a/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml b/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml
new file mode 100644
index 00000000000..88ba8028e2c
--- /dev/null
+++ b/changelogs/unreleased/48869-wiki-slugs-with-spaces.yml
@@ -0,0 +1,5 @@
+---
+title: Allow spaces in wiki markdown links when using CommonMark
+merge_request: 20417
+author:
+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-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/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml
new file mode 100644
index 00000000000..2fce00a662f
--- /dev/null
+++ b/changelogs/unreleased/49907-commits-and-merge-requests-does-not-list-all-files-when-one-file-exceeds-size-limits.yml
@@ -0,0 +1,5 @@
+---
+title: Fix merge requests not showing any diff files for big patches
+merge_request: 21125
+author:
+type: fixed
diff --git a/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml
new file mode 100644
index 00000000000..8057819b223
--- /dev/null
+++ b/changelogs/unreleased/50019-remove-redundant-header-from-metrics-page.yml
@@ -0,0 +1,5 @@
+---
+title: Remove redundant header from metrics page
+merge_request: 21282
+author:
+type: changed
diff --git a/changelogs/unreleased/50101-builds-dropdown.yml b/changelogs/unreleased/50101-builds-dropdown.yml
new file mode 100644
index 00000000000..9194b0e0d31
--- /dev/null
+++ b/changelogs/unreleased/50101-builds-dropdown.yml
@@ -0,0 +1,6 @@
+---
+title: Creates vue components for stage dropdowns and job list container for job log
+ view
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50101-commit-block.yml b/changelogs/unreleased/50101-commit-block.yml
new file mode 100644
index 00000000000..f6bad4c8154
--- /dev/null
+++ b/changelogs/unreleased/50101-commit-block.yml
@@ -0,0 +1,5 @@
+---
+title: Creates vue component for commit block in job log page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/50101-trigger.yml b/changelogs/unreleased/50101-trigger.yml
new file mode 100644
index 00000000000..df4243afa63
--- /dev/null
+++ b/changelogs/unreleased/50101-trigger.yml
@@ -0,0 +1,5 @@
+---
+title: Creates Vue component for trigger variables block in job log page
+merge_request:
+author:
+type: other
diff --git a/changelogs/unreleased/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/6010_remove_gemnasium_service.yml b/changelogs/unreleased/6010_remove_gemnasium_service.yml
new file mode 100644
index 00000000000..900e84c9eed
--- /dev/null
+++ b/changelogs/unreleased/6010_remove_gemnasium_service.yml
@@ -0,0 +1,5 @@
+---
+title: Remove Gemnasium service
+merge_request: 21185
+author:
+type: removed
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/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/fix_event_api_permissions.yml b/changelogs/unreleased/fix_event_api_permissions.yml
new file mode 100644
index 00000000000..dee280e93ad
--- /dev/null
+++ b/changelogs/unreleased/fix_event_api_permissions.yml
@@ -0,0 +1,5 @@
+---
+title: 'Events API now requires the read_user or api scope.'
+merge_request: 20627
+author: Warren Parad
+type: fixed
diff --git a/changelogs/unreleased/rails5-verbose-query-logs.yml b/changelogs/unreleased/rails5-verbose-query-logs.yml
new file mode 100644
index 00000000000..7585e75d30b
--- /dev/null
+++ b/changelogs/unreleased/rails5-verbose-query-logs.yml
@@ -0,0 +1,5 @@
+---
+title: 'Rails5: Enable verbose query logs'
+merge_request: 21231
+author: Jasper Maes
+type: other
diff --git a/changelogs/unreleased/repopulate_site_statistics.yml b/changelogs/unreleased/repopulate_site_statistics.yml
new file mode 100644
index 00000000000..1961088061d
--- /dev/null
+++ b/changelogs/unreleased/repopulate_site_statistics.yml
@@ -0,0 +1,5 @@
+---
+title: Migrate NULL wiki_access_level to correct number so we count active wikis correctly
+merge_request: 21030
+author:
+type: changed
diff --git a/changelogs/unreleased/rouge_3-2-1.yml b/changelogs/unreleased/rouge_3-2-1.yml
new file mode 100644
index 00000000000..b281a4f0e95
--- /dev/null
+++ b/changelogs/unreleased/rouge_3-2-1.yml
@@ -0,0 +1,5 @@
+---
+title: Update to Rouge 3.2.1, which includes a critical fix to the Perl Lexer
+merge_request: 21263
+author:
+type: changed
diff --git a/changelogs/unreleased/runners-online.yml b/changelogs/unreleased/runners-online.yml
new file mode 100644
index 00000000000..a732d9cb723
--- /dev/null
+++ b/changelogs/unreleased/runners-online.yml
@@ -0,0 +1,5 @@
+---
+title: Clarify current runners online text
+merge_request: 21151
+author: Ben Bodenmiller
+type: other
diff --git a/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml
new file mode 100644
index 00000000000..0e748c3a346
--- /dev/null
+++ b/changelogs/unreleased/sh-bump-gitaly-for-11-2.yml
@@ -0,0 +1,5 @@
+---
+title: Bump Gitaly to 0.117.1 for Rouge update
+merge_request: 21277
+author:
+type: security
diff --git a/config/initializers/active_record_verbose_query_logs.rb b/config/initializers/active_record_verbose_query_logs.rb
index 44f86fec7e0..1c5fbc8e830 100644
--- a/config/initializers/active_record_verbose_query_logs.rb
+++ b/config/initializers/active_record_verbose_query_logs.rb
@@ -47,7 +47,9 @@ module ActiveRecord
end
end
- unless Gitlab.rails5?
+ if Rails.version.start_with?("5.2")
+ raise "Remove this monkey patch: #{__FILE__}"
+ else
prepend(VerboseQueryLogs) unless Rails.env.production?
end
end
diff --git a/config/initializers/gettext_rails_i18n_patch.rb b/config/initializers/gettext_rails_i18n_patch.rb
index 49551319435..c1342f48ebd 100644
--- a/config/initializers/gettext_rails_i18n_patch.rb
+++ b/config/initializers/gettext_rails_i18n_patch.rb
@@ -1,5 +1,6 @@
require 'gettext_i18n_rails/haml_parser'
require 'gettext_i18n_rails_js/parser/javascript'
+require 'json'
VUE_TRANSLATE_REGEX = /((%[\w.-]+)(?:\s))?{{ (N|n|s)?__\((.*)\) }}/
@@ -36,6 +37,20 @@ module GettextI18nRailsJs
".vue"
].include? ::File.extname(file)
end
+
+ def collect_for(file)
+ gettext_messages_by_file[file] || []
+ end
+
+ private
+
+ def gettext_messages_by_file
+ @gettext_messages_by_file ||= JSON.parse(load_messages)
+ end
+
+ def load_messages
+ `node scripts/frontend/extract_gettext_all.js --all`
+ end
end
end
end
diff --git a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
index 668c22bb51c..8ebf1a5234d 100644
--- a/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
+++ b/db/migrate/20160712171823_remove_award_emojis_with_no_user.rb
@@ -16,6 +16,6 @@ class RemoveAwardEmojisWithNoUser < ActiveRecord::Migration
# disable_ddl_transaction!
def up
- AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all
+ AwardEmoji.joins('LEFT JOIN users ON users.id = user_id').where('users.id IS NULL').destroy_all # rubocop: disable DestroyAll
end
end
diff --git a/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb
new file mode 100644
index 00000000000..3b9b95ec9ca
--- /dev/null
+++ b/db/post_migrate/20180723130817_delete_inconsistent_internal_id_records.rb
@@ -0,0 +1,47 @@
+# frozen_string_literal: true
+class DeleteInconsistentInternalIdRecords < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # This migration cleans up any inconsistent records in internal_ids.
+ #
+ # That is, it deletes records that track a `last_value` that is
+ # smaller than the maximum internal id (usually `iid`) found in
+ # the corresponding model records.
+
+ def up
+ disable_statement_timeout do
+ delete_internal_id_records('issues', 'project_id')
+ delete_internal_id_records('merge_requests', 'project_id', 'target_project_id')
+ delete_internal_id_records('deployments', 'project_id')
+ delete_internal_id_records('milestones', 'project_id')
+ delete_internal_id_records('milestones', 'namespace_id', 'group_id')
+ delete_internal_id_records('ci_pipelines', 'project_id')
+ end
+ end
+
+ class InternalId < ActiveRecord::Base
+ self.table_name = 'internal_ids'
+ enum usage: { issues: 0, merge_requests: 1, deployments: 2, milestones: 3, epics: 4, ci_pipelines: 5 }
+ end
+
+ private
+
+ def delete_internal_id_records(base_table, scope_column_name, base_scope_column_name = scope_column_name)
+ sql = <<~SQL
+ SELECT id FROM ( -- workaround for MySQL
+ SELECT internal_ids.id FROM (
+ SELECT #{base_scope_column_name} AS #{scope_column_name}, max(iid) as maximum_iid from #{base_table} GROUP BY #{scope_column_name}
+ ) maxima JOIN internal_ids USING (#{scope_column_name})
+ WHERE internal_ids.usage=#{InternalId.usages.fetch(base_table)} AND maxima.maximum_iid > internal_ids.last_value
+ ) internal_ids
+ SQL
+
+ InternalId.where("id IN (#{sql})").tap do |ids| # rubocop:disable GitlabSecurity/SqlInjection
+ say "Deleting internal_id records for #{base_table}: #{ids.pluck(:project_id, :last_value)}" unless ids.empty?
+ end.delete_all
+ end
+end
diff --git a/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
new file mode 100644
index 00000000000..0a0a33299e4
--- /dev/null
+++ b/db/post_migrate/20180809195358_migrate_null_wiki_access_levels.rb
@@ -0,0 +1,32 @@
+# frozen_string_literal: true
+
+class MigrateNullWikiAccessLevels < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ class ProjectFeature < ActiveRecord::Base
+ include EachBatch
+
+ self.table_name = 'project_features'
+ end
+
+ def up
+ ProjectFeature.where(wiki_access_level: nil).each_batch do |relation|
+ relation.update_all(wiki_access_level: 20)
+ end
+
+ # We need to re-count wikis as previous attempt was not considering the NULLs.
+ transaction do
+ execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql? # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+
+ execute("UPDATE site_statistics SET wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)")
+ end
+ end
+
+ def down
+ # there is no way to rollback this change, there are no downsides in keeping migrated data.
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index 1288a98745c..9dc122b54b3 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20180808162000) do
+ActiveRecord::Schema.define(version: 20180809195358) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
diff --git a/doc/administration/high_availability/gitlab.md b/doc/administration/high_availability/gitlab.md
index 637d44d2823..b74554a5598 100644
--- a/doc/administration/high_availability/gitlab.md
+++ b/doc/administration/high_availability/gitlab.md
@@ -25,11 +25,11 @@ for each GitLab application server in your environment.
options. Here is an example snippet to add to `/etc/fstab`:
```
- 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
- 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
- 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
- 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
- 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+ 10.1.0.1:/var/opt/gitlab/.ssh /var/opt/gitlab/.ssh nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+ 10.1.0.1:/var/opt/gitlab/gitlab-rails/uploads /var/opt/gitlab/gitlab-rails/uploads nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+ 10.1.0.1:/var/opt/gitlab/gitlab-rails/shared /var/opt/gitlab/gitlab-rails/shared nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+ 10.1.0.1:/var/opt/gitlab/gitlab-ci/builds /var/opt/gitlab/gitlab-ci/builds nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
+ 10.1.0.1:/var/opt/gitlab/git-data /var/opt/gitlab/git-data nfs4 defaults,soft,rsize=1048576,wsize=1048576,noatime,nofail,lookupcache=positive 0 2
```
1. Create the shared directories. These may be different depending on your NFS
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/repository_storage_types.md b/doc/administration/repository_storage_types.md
index 88221db78f1..bd758c49eba 100644
--- a/doc/administration/repository_storage_types.md
+++ b/doc/administration/repository_storage_types.md
@@ -42,7 +42,7 @@ Registry, etc.
## Hashed Storage
> **Warning:** Hashed storage is in **Beta**. For the latest updates, check the
-> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/2821)
+> associated [issue](https://gitlab.com/gitlab-com/infrastructure/issues/3542)
> and please report any problems you encounter.
Hashed Storage is the new storage behavior we are rolling out with 10.0. Instead
diff --git a/doc/api/events.md b/doc/api/events.md
index 1b6c4d437dd..fb5ebb71a86 100644
--- a/doc/api/events.md
+++ b/doc/api/events.md
@@ -48,9 +48,11 @@ GitLab removes events older than 1 year from the events table for performance re
## List currently authenticated user's events
->**Note:** This endpoint was introduced in GitLab 9.3.
+>**Notes:**
+> This endpoint was introduced in GitLab 9.3.
+> `read_user` access was introduced in GitLab 11.3.
-Get a list of events for the authenticated user.
+Get a list of events for the authenticated user. Scope `read_user` or `api` is required.
```
GET /events
@@ -119,9 +121,11 @@ Example response:
### Get user contribution events
->**Note:** Documentation was formerly located in the [Users API pages][users-api].
+>**Notes:**
+> Documentation was formerly located in the [Users API pages][users-api].
+> `read_user` access was introduced in GitLab 11.3.
-Get the contribution events for the specified user, sorted from newest to oldest.
+Get the contribution events for the specified user, sorted from newest to oldest. Scope `read_user` or `api` is required.
```
GET /users/:id/events
diff --git a/doc/api/services.md b/doc/api/services.md
index efa173180bb..8c59b232b6d 100644
--- a/doc/api/services.md
+++ b/doc/api/services.md
@@ -401,48 +401,6 @@ Get Flowdock service settings for a project.
GET /projects/:id/services/flowdock
```
-## Gemnasium
-
-Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities.
-
-CAUTION: **Warning:**
-Gemnasium service integration has been deprecated in GitLab 11.0. Gemnasium has been
-[acquired by GitLab](https://about.gitlab.com/press/releases/2018-01-30-gemnasium-acquisition.html)
-in January 2018 and since May 15, 2018, the service provided by Gemnasium is no longer available.
-You can [migrate from Gemnasium to GitLab](https://docs.gitlab.com/ee/user/project/import/gemnasium.html)
-to keep monitoring your dependencies.
-
-### Create/Edit Gemnasium service
-
-Set Gemnasium service for a project.
-
-```
-PUT /projects/:id/services/gemnasium
-```
-
-Parameters:
-
-| Parameter | Type | Required | Description |
-| --------- | ---- | -------- | ----------- |
-| `api_key` | string | true | Your personal API KEY on gemnasium.com |
-| `token` | string | true | The project's slug on gemnasium.com |
-
-### Delete Gemnasium service
-
-Delete Gemnasium service for a project.
-
-```
-DELETE /projects/:id/services/gemnasium
-```
-
-### Get Gemnasium service settings
-
-Get Gemnasium service settings for a project.
-
-```
-GET /projects/:id/services/gemnasium
-```
-
## Hangouts Chat
Google GSuite team collaboration tool.
diff --git a/doc/ci/README.md b/doc/ci/README.md
index 7666219acb0..d782d64e971 100644
--- a/doc/ci/README.md
+++ b/doc/ci/README.md
@@ -76,6 +76,8 @@ learn how to leverage its potential even more.
- [Trigger pipelines on a schedule](../user/project/pipelines/schedules.md)
- [Kubernetes clusters](../user/project/clusters/index.md) - Integrate one or
more Kubernetes clusters to your project
+- [Interactive web terminal](interactive_web_terminal/index.md) - Open an interactive
+ web terminal to debug the running jobs
## GitLab CI/CD for Docker
diff --git a/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png
new file mode 100644
index 00000000000..199268a1486
--- /dev/null
+++ b/doc/ci/interactive_web_terminal/img/finished_job_with_terminal_open.png
Binary files differ
diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png
new file mode 100644
index 00000000000..b59c1b6bc43
--- /dev/null
+++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_page.png
Binary files differ
diff --git a/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png
new file mode 100644
index 00000000000..f92c6df07a1
--- /dev/null
+++ b/doc/ci/interactive_web_terminal/img/interactive_web_terminal_running_job.png
Binary files differ
diff --git a/doc/ci/interactive_web_terminal/index.md b/doc/ci/interactive_web_terminal/index.md
new file mode 100644
index 00000000000..507aceb27fa
--- /dev/null
+++ b/doc/ci/interactive_web_terminal/index.md
@@ -0,0 +1,52 @@
+# Getting started with interactive web terminals
+
+> Introduced in GitLab 11.3.
+
+CAUTION: **Warning:**
+Interactive web terminals are in beta, so they might not work properly and
+lack features. For more information [follow issue #25990](https://gitlab.com/gitlab-org/gitlab-ce/issues/25990).
+
+Interactive web terminals give the user access to a terminal in GitLab for
+running one-of commands for their CI pipeline.
+
+NOTE: **Note:**
+This is not available for the shared Runners on GitLab.com.
+To make use of this feature, you need to provide your
+[own Runner](https://docs.gitlab.com/runner/install/) and properly
+[configure it](#configuration).
+
+## Configuration
+
+Two things need to be configured for the interactive web terminal to work:
+
+- The Runner needs to have [`[session_server]` configured
+ properly][session-server]
+- Web terminals need to be
+ [enabled](../../administration/integration/terminal.md#enabling-and-disabling-terminal-support)
+
+## Debugging a running job
+
+NOTE: **Note:** Not all executors are
+[supported](https://docs.gitlab.com/runner/executors/#compatibility-chart).
+
+Sometimes, when a job is running, things don't go as you would expect, and it
+would be helpful if one can have a shell to aid debugging. When a job is
+running, on the right panel you can see a button `debug` that will open the terminal
+for the current job.
+
+![Example of job running with terminal
+available](img/interactive_web_terminal_running_job.png)
+
+When clicked, a new tab will open to the terminal page where you can access
+the terminal and type commands like a normal shell.
+
+![terminal of the job](img/interactive_web_terminal_page.png)
+
+If you have the terminal open and the job has finished with its tasks, the
+terminal will block the job from finishing for the duration configured in
+[`[session_server].terminal_max_retention_time`][session-server] until you
+close the terminal window.
+
+![finished job with terminal open](img/finished_job_with_terminal_open.png)
+
+[session-server]: https://docs.gitlab.com/runner/configuration/advanced-configuration.html#the-session_server-section
diff --git a/doc/ci/yaml/README.md b/doc/ci/yaml/README.md
index ef740ab1c5e..abba748db8b 100644
--- a/doc/ci/yaml/README.md
+++ b/doc/ci/yaml/README.md
@@ -1075,8 +1075,10 @@ keep artifacts forever.
After their expiry, artifacts are deleted hourly by default (via a cron job),
and are not accessible anymore.
-The value of `expire_in` is an elapsed time. Examples of parsable values:
+The value of `expire_in` is an elapsed time in seconds, unless a unit is
+provided. Examples of parsable values:
+- '42'
- '3 mins 4 sec'
- '2 hrs 20 min'
- '2h20min'
diff --git a/doc/development/README.md b/doc/development/README.md
index fed3903c771..ee9a9852205 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -55,7 +55,13 @@ description: 'Learn how to contribute to GitLab.'
- [Merge request performance guidelines](merge_request_performance_guidelines.md)
for ensuring merge requests do not negatively impact GitLab performance
-## Databases guides
+## Database guides
+
+### Tooling
+
+- [Understanding EXPLAIN plans](understanding_explain_plans.md)
+- [explain.depesz.com](https://explain.depesz.com/) for visualising the output
+ of `EXPLAIN`
### Migrations
diff --git a/doc/development/ee_features.md b/doc/development/ee_features.md
index 32de741c9fe..1cd873b6fe3 100644
--- a/doc/development/ee_features.md
+++ b/doc/development/ee_features.md
@@ -258,6 +258,31 @@ end
[`extend ::Gitlab::Utils::Override`]: utilities.md#override
+##### Overriding CE class methods
+
+The same applies to class methods, except we want to use
+`ActiveSupport::Concern` and put `extend ::Gitlab::Utils::Override`
+within the block of `class_methods`. Here's an example:
+
+```ruby
+module EE
+ module Groups
+ module GroupMembersController
+ extend ActiveSupport::Concern
+
+ class_methods do
+ extend ::Gitlab::Utils::Override
+
+ override :admin_not_required_endpoints
+ def admin_not_required_endpoints
+ super.concat(%i[update override])
+ end
+ end
+ end
+ end
+end
+```
+
#### Use self-descriptive wrapper methods
When it's not possible/logical to modify the implementation of a
@@ -665,6 +690,9 @@ module EE
extend ActiveSupport::Concern
class_methods do
+ extend ::Gitlab::Utils::Override
+
+ override :update_params_at_least_one_of
def update_params_at_least_one_of
super.push(*%i[
squash
diff --git a/doc/development/testing_guide/smoke.md b/doc/development/testing_guide/smoke.md
new file mode 100644
index 00000000000..3f2843cba6e
--- /dev/null
+++ b/doc/development/testing_guide/smoke.md
@@ -0,0 +1,16 @@
+# Smoke Tests
+
+It is imperative in any testing suite that we have Smoke Tests. In short, smoke tests are will run quick sanity
+end-to-end functional tests from GitLab QA and are designed to run against the specified environment to ensure that
+basic functionality is working.
+
+Currently, our suite consists of this basic functionality coverage:
+
+- User Login (Standard Auth)
+- Project Creation
+- Issue Creation
+- Merge Request Creation
+
+---
+
+[Return to Testing documentation](index.md)
diff --git a/doc/development/testing_guide/testing_levels.md b/doc/development/testing_guide/testing_levels.md
index 07ced36f0c1..4403072e96f 100644
--- a/doc/development/testing_guide/testing_levels.md
+++ b/doc/development/testing_guide/testing_levels.md
@@ -120,6 +120,14 @@ running feature tests (i.e. using Capybara) against it.
The actual test scenarios and steps are [part of GitLab Rails] so that they're
always in-sync with the codebase.
+### Smoke tests
+
+Smoke tests are quick tests that may be run at any time (especially after the pre-deployment migrations).
+
+Much like feature tests - these tests run against the UI and ensure that basic functionality is working.
+
+> See [Smoke Tests](smoke.md) for more information.
+
Read a separate document about [end-to-end tests](end_to_end_tests.md) to
learn more.
diff --git a/doc/development/understanding_explain_plans.md b/doc/development/understanding_explain_plans.md
new file mode 100644
index 00000000000..adf8795a5e3
--- /dev/null
+++ b/doc/development/understanding_explain_plans.md
@@ -0,0 +1,676 @@
+# Understanding EXPLAIN plans
+
+PostgreSQL allows you to obtain query plans using the `EXPLAIN` command. This
+command can be invaluable when trying to determine how a query will perform.
+You can use this command directly in your SQL query, as long as the query starts
+with it:
+
+```sql
+EXPLAIN
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+When running this on GitLab.com, we are presented with the following output:
+
+```
+Aggregate (cost=922411.76..922411.77 rows=1 width=8)
+ -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+```
+
+When using _just_ `EXPLAIN`, PostgreSQL won't actually execute our query,
+instead it produces an _estimated_ execution plan based on the available
+statistics. This means the actual plan can differ quite a bit. Fortunately,
+PostgreSQL provides us with the option to execute the query as well. To do so,
+we need to use `EXPLAIN ANALYZE` instead of just `EXPLAIN`:
+
+```sql
+EXPLAIN ANALYZE
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+This will produce:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+As we can see this plan is quite different, and includes a lot more data. Let's
+discuss this step by step.
+
+Because `EXPLAIN ANALYZE` executes the query, care should be taken when using a
+query that will write data or might time out. If the query modifies data,
+consider wrapping it in a transaction that rolls back automatically like so:
+
+```sql
+BEGIN;
+EXPLAIN ANALYZE
+DELETE FROM users WHERE id = 1;
+ROLLBACK;
+```
+
+The `EXPLAIN` command also takes additional options, such as `BUFFERS`:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+This will then produce:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ Buffers: shared hit=208846
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+For more information, refer to the official [EXPLAIN
+documentation](https://www.postgresql.org/docs/current/static/sql-explain.html).
+
+## Nodes
+
+Every query plan consists of nodes. Nodes can be nested, and are executed from
+the inside out. This means that the innermost node is executed before an outer
+node. This can be best thought of as nested function calls, returning their
+results as they unwind. For example, a plan starting with an `Aggregate`
+followed by a `Nested Loop`, followed by an `Index Only scan` can be thought of
+as the following Ruby code:
+
+```ruby
+aggregate(
+ nested_loop(
+ index_only_scan()
+ index_only_scan()
+ )
+)
+```
+
+Nodes are indicated using a `->` followed by the type of node taken. For
+example:
+
+```
+Aggregate (cost=922411.76..922411.77 rows=1 width=8)
+ -> Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+```
+
+Here the first node executed is `Seq scan on projects`. The `Filter:` is an
+additional filter applied to the results of the node. A filter is very similar
+to Ruby's `Array#select`: it takes the input rows, applies the filter, and
+produces a new list of rows. Once the node is done, we perform the `Aggregate`
+above it.
+
+Nested nodes will look like this:
+
+```
+Aggregate (cost=176.97..176.98 rows=1 width=8) (actual time=0.252..0.252 rows=1 loops=1)
+ Buffers: shared hit=155
+ -> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1)
+ Buffers: shared hit=155
+ -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1)
+ Index Cond: (id < 100)
+ Heap Fetches: 0
+ -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36)
+ Index Cond: (id = users_1.id)
+ Heap Fetches: 0
+Planning time: 2.585 ms
+Execution time: 0.310 ms
+```
+
+Here we first perform two separate "Index Only" scans, followed by performing a
+"Nested Loop" on the result of these two scans.
+
+## Node statistics
+
+Each node in a plan has a set of associated statistics, such as the cost, the
+number of rows produced, the number of loops performed, and more. For example:
+
+```
+Seq Scan on projects (cost=0.00..908044.47 rows=5746914 width=0)
+```
+
+Here we can see that our cost ranges from `0.00..908044.47` (we'll cover this in
+a moment), and we estimate (since we're using `EXPLAIN` and not `EXPLAIN
+ANALYZE`) a total of 5,746,914 rows to be produced by this node. The `width`
+statistics describes the estimated width of each row, in bytes.
+
+The `costs` field specifies how expensive a node was. The cost is measured in
+arbitrary units determined by the query planner's cost parameters. What
+influences the costs depends on a variety of settings, such as `seq_page_cost`,
+`cpu_tuple_cost`, and various others.
+The format of the costs field is as follows:
+
+```
+STARTUP COST..TOTAL COST
+```
+
+The startup cost states how expensive it was to start the node, with the total
+cost describing how expensive the entire node was. In general: the greater the
+values, the more expensive the node.
+
+When using `EXPLAIN ANALYZE`, these statistics will also include the actual time
+(in milliseconds) spent, and other runtime statistics (e.g. the actual number of
+produced rows):
+
+```
+Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+```
+
+Here we can see we estimated 5,746,969 rows to be returned, but in reality we
+returned 5,746,940 rows. We can also see that _just_ this sequential scan took
+2.98 seconds to run.
+
+Using `EXPLAIN (ANALYZE, BUFFERS)` will also give us information about the
+number of rows removed by a filter, the number of buffers used, and more. For
+example:
+
+```
+Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+```
+
+Here we can see that our filter has to remove 65,677 rows, and that we use
+208,846 buffers. Each buffer in PostgreSQL is 8 KB (8192 bytes), meaning our
+above node uses *1.6 GB of buffers*. That's a lot!
+
+## Node types
+
+There are quite a few different types of nodes, so we only cover some of the
+more common ones here.
+
+A full list of all the available nodes and their descriptions can be found in
+the [PostgreSQL source file
+"plannodes.h"](https://github.com/postgres/postgres/blob/master/src/include/nodes/plannodes.h)
+
+### Seq Scan
+
+A sequential scan over (a chunk of) a database table. This is like using
+`Array#each`, but on a database table. Sequential scans can be quite slow when
+retrieving lots of rows, so it's best to avoid these for large tables.
+
+### Index Only Scan
+
+A scan on an index that did not require fetching anything from the table. In
+certain cases an index only scan may still fetch data from the table, in this
+case the node will include a `Heap Fetches:` statistic.
+
+### Index Scan
+
+A scan on an index that required retrieving some data from the table.
+
+### Bitmap Index Scan and Bitmap Heap scan
+
+Bitmap scans fall between sequential scans and index scans. These are typically
+used when we would read too much data from an index scan, but too little to
+perform a sequential scan. A bitmap scan uses what is known as a [bitmap
+index](https://en.wikipedia.org/wiki/Bitmap_index) to perform its work.
+
+The [source code of PostgreSQL](https://github.com/postgres/postgres/blob/1c2cb2744bf3d8ad751cd5cf3b347f10f48492b3/src/include/nodes/plannodes.h#L446-L457)
+states the following on bitmap scans:
+
+> Bitmap Index Scan delivers a bitmap of potential tuple locations; it does not
+> access the heap itself. The bitmap is used by an ancestor Bitmap Heap Scan
+> node, possibly after passing through intermediate Bitmap And and/or Bitmap Or
+> nodes to combine it with the results of other Bitmap Index Scans.
+
+### Limit
+
+Applies a `LIMIT` on the input rows.
+
+### Sort
+
+Sorts the input rows as specified using an `ORDER BY` statement.
+
+### Nested Loop
+
+A nested loop will execute its child nodes for every row produced by a node that
+precedes it. For example:
+
+```
+-> Nested Loop (cost=0.86..176.75 rows=87 width=0) (actual time=0.035..0.249 rows=36 loops=1)
+ Buffers: shared hit=155
+ -> Index Only Scan using users_pkey on users users_1 (cost=0.43..4.95 rows=87 width=4) (actual time=0.029..0.123 rows=36 loops=1)
+ Index Cond: (id < 100)
+ Heap Fetches: 0
+ -> Index Only Scan using users_pkey on users (cost=0.43..1.96 rows=1 width=4) (actual time=0.003..0.003 rows=1 loops=36)
+ Index Cond: (id = users_1.id)
+ Heap Fetches: 0
+```
+
+Here the first child node (`Index Only Scan using users_pkey on users users_1`)
+produces 36 rows, and is executed once (`rows=36 loops=1`). The next node
+produces 1 row (`rows=1`), but is repeated 36 times (`loops=36`). This is
+because the previous node produced 36 rows.
+
+This means that nested loops can quickly slow the query down if the various
+child nodes keep producing many rows.
+
+## Optimising queries
+
+With that out of the way, let's see how we can optimise a query. Let's use the
+following query as an example:
+
+```sql
+SELECT COUNT(*)
+FROM users
+WHERE twitter != '';
+```
+
+This query simply counts the number of users that have a Twitter profile set.
+Let's run this using `EXPLAIN (ANALYZE, BUFFERS)`:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM users
+WHERE twitter != '';
+```
+
+This will produce the following plan:
+
+```
+Aggregate (cost=845110.21..845110.22 rows=1 width=8) (actual time=1271.157..1271.158 rows=1 loops=1)
+ Buffers: shared hit=202662
+ -> Seq Scan on users (cost=0.00..844969.99 rows=56087 width=0) (actual time=0.019..1265.883 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487813
+ Buffers: shared hit=202662
+Planning time: 0.390 ms
+Execution time: 1271.180 ms
+```
+
+From this query plan we can see the following:
+
+1. We need to perform a sequential scan on the `users` table.
+1. This sequential scan filters out 2,487,813 rows using a `Filter`.
+1. We use 202,622 buffers, which equals 1.58 GB of memory.
+1. It takes us 1.2 seconds to do all of this.
+
+Considering we are just counting users, that's quite expensive!
+
+Before we start making any changes, let's see if there are any existing indexes
+on the `users` table that we might be able to use. We can obtain this
+information by running `\d users` in a `psql` console, then scrolling down to
+the `Indexes:` section:
+
+```
+Indexes:
+ "users_pkey" PRIMARY KEY, btree (id)
+ "users_confirmation_token_key" UNIQUE CONSTRAINT, btree (confirmation_token)
+ "users_email_key" UNIQUE CONSTRAINT, btree (email)
+ "users_reset_password_token_key" UNIQUE CONSTRAINT, btree (reset_password_token)
+ "index_on_users_lower_email" btree (lower(email::text))
+ "index_on_users_lower_username" btree (lower(username::text))
+ "index_on_users_name_lower" btree (lower(name::text))
+ "index_users_on_admin" btree (admin)
+ "index_users_on_created_at" btree (created_at)
+ "index_users_on_email_trigram" gin (email gin_trgm_ops)
+ "index_users_on_feed_token" btree (feed_token)
+ "index_users_on_ghost" btree (ghost)
+ "index_users_on_incoming_email_token" btree (incoming_email_token)
+ "index_users_on_name" btree (name)
+ "index_users_on_name_trigram" gin (name gin_trgm_ops)
+ "index_users_on_state" btree (state)
+ "index_users_on_state_and_internal_attrs" btree (state) WHERE ghost <> true AND support_bot <> true
+ "index_users_on_support_bot" btree (support_bot)
+ "index_users_on_username" btree (username)
+ "index_users_on_username_trigram" gin (username gin_trgm_ops)
+```
+
+Here we can see there is no index on the `twitter` column, which means
+PostgreSQL has to perform a sequential scan in this case. Let's try to fix this
+by adding the following index:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter);
+```
+
+If we now re-run our query using `EXPLAIN (ANALYZE, BUFFERS)` we get the
+following plan:
+
+```
+Aggregate (cost=61002.82..61002.83 rows=1 width=8) (actual time=297.311..297.312 rows=1 loops=1)
+ Buffers: shared hit=51854 dirtied=19
+ -> Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487830
+ Heap Fetches: 26037
+ Buffers: shared hit=51854 dirtied=19
+Planning time: 0.191 ms
+Execution time: 297.334 ms
+```
+
+Now it takes just under 300 milliseconds to get our data, instead of 1.2
+seconds. However, we still use 51,854 buffers, which is about 400 MB of memory.
+300 milliseconds is also quite slow for such a simple query. To understand why
+this query is still expensive, let's take a look at the following:
+
+```
+Index Only Scan using twitter_test on users (cost=0.43..60873.13 rows=51877 width=0) (actual time=279.184..293.532 rows=51833 loops=1)
+ Filter: ((twitter)::text <> ''::text)
+ Rows Removed by Filter: 2487830
+```
+
+We start with an index only scan on our index, but we somehow still apply a
+`Filter` that filters out 2,487,830 rows. Why is that? Well, let's look at how
+we created the index:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter);
+```
+
+We simply told PostgreSQL to index all possible values of the `twitter` column,
+even empty strings. Our query in turn uses `WHERE twitter != ''`. This means
+that the index does improve things, as we don't need to do a sequential scan,
+but we may still encounter empty strings. This means PostgreSQL _has_ to apply a
+Filter on the index results to get rid of those values.
+
+Fortunately, we can improve this even further using "partial indexes". Partial
+indexes are indexes with a `WHERE` condition that is applied when indexing data.
+For example:
+
+```sql
+CREATE INDEX CONCURRENTLY some_index ON users (email) WHERE id < 100
+```
+
+This index would only index the `email` value of rows that match `WHERE id <
+100`. We can use partial indexes to change our Twitter index to the following:
+
+```sql
+CREATE INDEX CONCURRENTLY twitter_test ON users (twitter) WHERE twitter != '';
+```
+
+Once created, if we run our query again we will be given the following plan:
+
+```
+Aggregate (cost=1608.26..1608.27 rows=1 width=8) (actual time=19.821..19.821 rows=1 loops=1)
+ Buffers: shared hit=44036
+ -> Index Only Scan using twitter_test on users (cost=0.41..1479.71 rows=51420 width=0) (actual time=0.023..15.514 rows=51833 loops=1)
+ Heap Fetches: 1208
+ Buffers: shared hit=44036
+Planning time: 0.123 ms
+Execution time: 19.848 ms
+```
+
+That's _a lot_ better! Now it only takes 20 milliseconds to get the data, and we
+only use about 344 MB of buffers (instead of the original 1.58 GB). The reason
+this works is that now PostgreSQL no longer needs to apply a `Filter`, as the
+index only contains `twitter` values that are not empty.
+
+Keep in mind that you shouldn't just add partial indexes every time you want to
+optimise a query. Every index has to be updated for every write, and they may
+require quite a bit of space, depending on the amount of indexed data. As a
+result, first check if there are any existing indexes you may be able to reuse.
+If there aren't any, check if you can perhaps slightly change an existing one to
+fit both the existing and new queries. Only add a new index if none of the
+existing indexes can be used in any way.
+
+## Queries that can't be optimised
+
+Now that we have seen how to optimise a query, let's look at another query that
+we might not be able to optimise:
+
+```sql
+EXPLAIN (ANALYZE, BUFFERS)
+SELECT COUNT(*)
+FROM projects
+WHERE visibility_level IN (0, 20);
+```
+
+The output of `EXPLAIN (ANALYZE, BUFFERS)` is as follows:
+
+```
+Aggregate (cost=922420.60..922420.61 rows=1 width=8) (actual time=3428.535..3428.535 rows=1 loops=1)
+ Buffers: shared hit=208846
+ -> Seq Scan on projects (cost=0.00..908053.18 rows=5746969 width=0) (actual time=0.041..2987.606 rows=5746940 loops=1)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 65677
+ Buffers: shared hit=208846
+Planning time: 2.861 ms
+Execution time: 3428.596 ms
+```
+
+Looking at the output we see the following Filter:
+
+```
+Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+Rows Removed by Filter: 65677
+```
+
+Looking at the number of rows removed by the filter, we may be tempted to add an
+index on `projects.visibility_level` to somehow turn this Sequential scan +
+filter into an index-only scan.
+
+Unfortunately, doing so is unlikely to improve anything. Contrary to what some
+might believe, an index being present _does not guarantee_ that PostgreSQL will
+actually use it. For example, when doing a `SELECT * FROM projects` it is much
+cheaper to just scan the entire table, instead of using an index and then
+fetching data from the table. In such cases PostgreSQL may decide to not use an
+index.
+
+Second, let's think for a moment what our query does: it gets all projects with
+visibility level 0 or 20. In the above plan we can see this produces quite a lot
+of rows (5,745,940), but how much is that relative to the total? Let's find out
+by running the following query:
+
+```sql
+SELECT visibility_level, count(*) AS amount
+FROM projects
+GROUP BY visibility_level
+ORDER BY visibility_level ASC;
+```
+
+For GitLab.com this produces:
+
+```
+ visibility_level | amount
+------------------+---------
+ 0 | 5071325
+ 10 | 65678
+ 20 | 674801
+```
+
+Here the total number of projects is 5,811,804, and 5,746,126 of those are of
+level 0 or 20. That's 98% of the entire table!
+
+So no matter what we do, this query will retrieve 98% of the entire table. Since
+most time is spent doing exactly that, there isn't really much we can do to
+improve this query, other than _not_ running it at all.
+
+What is important here is that while some may recommend to straight up add an
+index the moment you see a sequential scan, it is _much more important_ to first
+understand what your query does, how much data it retrieves, and so on. After
+all, you can not optimise something you do not understand.
+
+### Cardinality and selectivity
+
+Earlier we saw that our query had to retrieve 98% of the rows in the table.
+There are two terms commonly used for databases: cardinality, and selectivity.
+Cardinality refers to the number of unique values in a particular column in a
+table.
+
+Selectivity is the number of unique values produced by an operation (e.g. an
+index scan or filter), relative to the total number of rows. The higher the
+selectivity, the more likely PostgreSQL is able to use an index.
+
+In the above example, there are only 3 unique values: 0, 10, and 20. This means
+the cardinality is 3. The selectivity in turn is also very low: 0.0000003% (2 /
+5,811,804), because our `Filter` only filters using two values (`0` and `20`).
+With such a low selectivity value it's not surprising that PostgreSQL decides
+using an index is not worth it, because it would produce almost no unique rows.
+
+## Rewriting queries
+
+So the above query can't really be optimised as-is, or at least not much. But
+what if we slightly change the purpose of it? What if instead of retrieving all
+projects with `visibility_level` 0 or 20, we retrieve those that a user
+interacted with somehow?
+
+Fortunately, GitLab has an answer for this, and it's a table called
+`user_interacted_projects`. This table has the following schema:
+
+```
+Table "public.user_interacted_projects"
+ Column | Type | Modifiers
+------------+---------+-----------
+ user_id | integer | not null
+ project_id | integer | not null
+Indexes:
+ "index_user_interacted_projects_on_project_id_and_user_id" UNIQUE, btree (project_id, user_id)
+ "index_user_interacted_projects_on_user_id" btree (user_id)
+Foreign-key constraints:
+ "fk_rails_0894651f08" FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
+ "fk_rails_722ceba4f7" FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE
+```
+
+Let's rewrite our query to JOIN this table onto our projects, and get the
+projects for a specific user:
+
+```sql
+EXPLAIN ANALYZE
+SELECT COUNT(*)
+FROM projects
+INNER JOIN user_interacted_projects ON user_interacted_projects.project_id = projects.id
+WHERE projects.visibility_level IN (0, 20)
+AND user_interacted_projects.user_id = 1;
+```
+
+What we do here is the following:
+
+1. Get our projects.
+1. INNER JOIN `user_interacted_projects`, meaning we're only left with rows in
+ `projects` that have a corresponding row in `user_interacted_projects`.
+1. Limit this to the projects with `visibility_level` of 0 or 20, and to
+ projects that the user with ID 1 interacted with.
+
+If we run this query we get the following plan:
+
+```
+ Aggregate (cost=871.03..871.04 rows=1 width=8) (actual time=9.763..9.763 rows=1 loops=1)
+ -> Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1)
+ -> Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1)
+ Index Cond: (user_id = 1)
+ -> Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+ Index Cond: (id = user_interacted_projects.project_id)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 0
+ Planning time: 2.614 ms
+ Execution time: 9.809 ms
+```
+
+Here it only took us just under 10 milliseconds to get the data. We can also see
+we're retrieving far fewer projects:
+
+```
+Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+ Index Cond: (id = user_interacted_projects.project_id)
+ Filter: (visibility_level = ANY ('{0,20}'::integer[]))
+ Rows Removed by Filter: 0
+```
+
+Here we see we perform 145 loops (`loops=145`), with every loop producing 1 row
+(`rows=1`). This is much less than before, and our query performs much better!
+
+If we look at the plan we also see our costs are very low:
+
+```
+Index Scan using projects_pkey on projects (cost=0.43..3.45 rows=1 width=4) (actual time=0.049..0.050 rows=1 loops=145)
+```
+
+Here our cost is only 3.45, and it only takes us 0.050 milliseconds to do so.
+The next index scan is a bit more expensive:
+
+```
+Index Scan using index_user_interacted_projects_on_user_id on user_interacted_projects (cost=0.43..160.71 rows=205 width=4) (actual time=0.939..2.508 rows=145 loops=1)
+```
+
+Here the cost is 160.71 (`cost=0.43..160.71`), taking about 2.5 milliseconds
+(based on the output of `actual time=....`).
+
+The most expensive part here is the "Nested Loop" that acts upon the result of
+these two index scans:
+
+```
+Nested Loop (cost=0.86..870.52 rows=203 width=0) (actual time=1.072..9.748 rows=143 loops=1)
+```
+
+Here we had to perform 870.52 disk page fetches for 203 rows, 9.748
+milliseconds, producing 143 rows in a single loop.
+
+The key takeaway here is that sometimes you have to rewrite (parts of) a query
+to make it better. Sometimes that means having to slightly change your feature
+to accommodate for better performance.
+
+## What makes a bad plan
+
+This is a bit of a difficult question to answer, because the definition of "bad"
+is relative to the problem you are trying to solve. However, some patterns are
+best avoided in most cases, such as:
+
+* Sequential scans on large tables
+* Filters that remove a lot of rows
+* Performing a certain step (e.g. an index scan) that requires _a lot_ of
+ buffers (e.g. more than 512 MB for GitLab.com).
+
+As a general guideline, aim for a query that:
+
+1. Takes no more than 10 milliseconds. Our target time spent in SQL per request
+ is around 100 milliseconds, so every query should be as fast as possible.
+1. Does not use an excessive number of buffers, relative to the workload. For
+ example, retrieving ten rows shouldn't require 1 GB of buffers.
+1. Does not spend a long amount of time performing disk IO operations. The
+ setting `track_io_timing` must be enabled for this data to be included in the
+ output of `EXPLAIN ANALYZE`.
+1. Applies a `LIMIT` when retrieving rows without aggregating them, such as
+ `SELECT * FROM users`.
+1. Doesn't use a `Filter` to filter out too many rows, especially if the query
+ does not use a `LIMIT` to limit the number of returned rows. Filters can
+ usually be removed by adding a (partial) index.
+
+These are _guidelines_ and not hard requirements, as different needs may require
+different queries. The only _rule_ is that you _must always measure_ your query
+(preferably using a production-like database) using `EXPLAIN (ANALYZE, BUFFERS)`
+and related tools such as:
+
+* <https://explain.depesz.com/>
+* <http://tatiyants.com/postgres-query-plan-visualization/>
+
+GitLab employees can also use our chatops solution, available in Slack using the
+`/chatops` slash command. You can use chatops to get a query plan by running the
+following:
+
+```
+/chatops run explain SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20)
+```
+
+Visualising the plan using <https://explain.depesz.com/> is also supported:
+
+```
+/chatops run explain --visual SELECT COUNT(*) FROM projects WHERE visibility_level IN (0, 20)
+```
+
+Quoting the query is not necessary.
+
+For more information about the available options, run:
+
+```
+/chatops run explain --help
+```
diff --git a/doc/install/kubernetes/gitlab_omnibus.md b/doc/install/kubernetes/gitlab_omnibus.md
index 9aee6b9dc74..c2c8a7a92fd 100644
--- a/doc/install/kubernetes/gitlab_omnibus.md
+++ b/doc/install/kubernetes/gitlab_omnibus.md
@@ -120,7 +120,7 @@ gitlabConfigStorageSize: 1Gi
Ingress routing and SSL are automatically configured within this Chart. An NGINX ingress is provisioned and configured, and will route traffic to any service. SSL certificates are automatically created and configured by [kube-lego](https://github.com/kubernetes/charts/tree/master/stable/kube-lego).
> **Note:**
-Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) and [nip.io](http://nip.io) are unlikely to work.
+Let's Encrypt limits a single TLD to five certificate requests within a single week. This means that common DNS wildcard services like [nip.io](http://nip.io) are unlikely to work.
## Installing GitLab using the Helm Chart
> **Note:**
diff --git a/doc/integration/README.md b/doc/integration/README.md
index 54e78bdef54..8a93d4cb84b 100644
--- a/doc/integration/README.md
+++ b/doc/integration/README.md
@@ -30,7 +30,7 @@ Bitbucket.org account
## Project services
-Integration with services such as Campfire, Flowdock, Gemnasium, HipChat,
+Integration with services such as Campfire, Flowdock, HipChat,
Pivotal Tracker, and Slack are available in the form of a [Project Service][].
[Project Service]: ../user/project/integrations/project_services.md
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/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/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/integrations/project_services.md b/doc/user/project/integrations/project_services.md
index 05ee1b4e6d7..efb0381d7aa 100644
--- a/doc/user/project/integrations/project_services.md
+++ b/doc/user/project/integrations/project_services.md
@@ -34,7 +34,6 @@ Click on the service links to see further configuration instructions and details
| [Emails on push](emails_on_push.md) | Email the commits and diff of each push to a list of recipients |
| External Wiki | Replaces the link to the internal wiki with a link to an external wiki |
| Flowdock | Flowdock is a collaboration web app for technical teams |
-| Gemnasium _(Has been deprecated in GitLab 11.0)_ | Gemnasium monitors your project dependencies and alerts you about updates and security vulnerabilities |
| [Hangouts Chat](hangouts_chat.md) | Receive events notifications in Google Hangouts Chat |
| [HipChat](hipchat.md) | Private group chat and IM |
| [Irker (IRC gateway)](irker.md) | Send IRC messages, on update, to a list of recipients through an Irker gateway |
diff --git a/doc/user/project/integrations/webhooks.md b/doc/user/project/integrations/webhooks.md
index 77fa517b5b1..770b1810da1 100644
--- a/doc/user/project/integrations/webhooks.md
+++ b/doc/user/project/integrations/webhooks.md
@@ -1102,6 +1102,7 @@ X-Gitlab-Event: Build Hook
"build_finished_at": null,
"build_duration": null,
"build_allow_failure": false,
+ "build_failure_reason": "script_failure",
"project_id": 380,
"project_name": "gitlab-org/gitlab-test",
"user": {
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/lib/api/events.rb b/lib/api/events.rb
index fc4ba5a3188..a415508a632 100644
--- a/lib/api/events.rb
+++ b/lib/api/events.rb
@@ -1,6 +1,7 @@
module API
class Events < Grape::API
include PaginationParams
+ include APIGuard
helpers do
params :event_filter_params do
@@ -24,6 +25,8 @@ module API
end
resource :events do
+ allow_access_with_scope :read_user, if: -> (request) { request.get? }
+
desc "List currently authenticated user's events" do
detail 'This feature was introduced in GitLab 9.3.'
success Entities::Event
@@ -46,6 +49,8 @@ module API
requires :id, type: String, desc: 'The ID or Username of the user'
end
resource :users do
+ allow_access_with_scope :read_user, if: -> (request) { request.get? }
+
desc 'Get the contribution events of a specified user' do
detail 'This feature was introduced in GitLab 8.13.'
success Entities::Event
diff --git a/lib/api/services.rb b/lib/api/services.rb
index 1f2bf546cd7..d1a5ee7db35 100644
--- a/lib/api/services.rb
+++ b/lib/api/services.rb
@@ -354,20 +354,6 @@ module API
desc: 'Flowdock token'
}
],
- 'gemnasium' => [
- {
- required: true,
- name: :api_key,
- type: String,
- desc: 'Your personal API key on gemnasium.com'
- },
- {
- required: true,
- name: :token,
- type: String,
- desc: "The project's slug on gemnasium.com"
- }
- ],
'hangouts-chat' => [
{
required: true,
@@ -695,7 +681,6 @@ module API
EmailsOnPushService,
ExternalWikiService,
FlowdockService,
- GemnasiumService,
HangoutsChatService,
HipchatService,
IrkerService,
diff --git a/lib/banzai/filter/spaced_link_filter.rb b/lib/banzai/filter/spaced_link_filter.rb
new file mode 100644
index 00000000000..574a8a6c7a5
--- /dev/null
+++ b/lib/banzai/filter/spaced_link_filter.rb
@@ -0,0 +1,77 @@
+# frozen_string_literal: true
+
+require 'uri'
+
+module Banzai
+ module Filter
+ # HTML Filter for markdown links with spaces in the URLs
+ #
+ # Based on Banzai::Filter::AutolinkFilter
+ #
+ # CommonMark does not allow spaces in the url portion of a link.
+ # For example, `[example](page slug)` is not valid. However,
+ # in our wikis, we support (via RedCarpet) this type of link, allowing
+ # wiki pages to be easily linked by their title. This filter adds that functionality.
+ # The intent is for this to only be used in Wikis - in general, we want
+ # to adhere to CommonMark's spec.
+ #
+ class SpacedLinkFilter < HTML::Pipeline::Filter
+ include ActionView::Helpers::TagHelper
+
+ # Pattern to match a standard markdown link
+ #
+ # Rubular: http://rubular.com/r/z9EAHxYmKI
+ LINK_PATTERN = /\[([^\]]+)\]\(([^)"]+)(?: \"([^\"]+)\")?\)/
+
+ # Text matching LINK_PATTERN inside these elements will not be linked
+ IGNORE_PARENTS = %w(a code kbd pre script style).to_set
+
+ # The XPath query to use for finding text nodes to parse.
+ TEXT_QUERY = %Q(descendant-or-self::text()[
+ not(#{IGNORE_PARENTS.map { |p| "ancestor::#{p}" }.join(' or ')})
+ and contains(., ']\(')
+ ]).freeze
+
+ def call
+ return doc if context[:markdown_engine] == :redcarpet
+
+ doc.xpath(TEXT_QUERY).each do |node|
+ content = node.to_html
+
+ next unless content.match(LINK_PATTERN)
+
+ html = spaced_link_filter(content)
+
+ next if html == content
+
+ node.replace(html)
+ end
+
+ doc
+ end
+
+ private
+
+ def spaced_link_match(link)
+ match = LINK_PATTERN.match(link)
+ return link unless match && match[1] && match[2]
+
+ # escape the spaces in the url so that it's a valid markdown link,
+ # then run it through the markdown processor again, let it do its magic
+ text = match[1]
+ new_link = match[2].gsub(' ', '%20')
+ title = match[3] ? " \"#{match[3]}\"" : ''
+ html = Banzai::Filter::MarkdownFilter.call("[#{text}](#{new_link}#{title})", context)
+
+ # link is wrapped in a <p>, so strip that off
+ html.sub('<p>', '').chomp('</p>')
+ end
+
+ def spaced_link_filter(text)
+ Gitlab::StringRegexMarker.new(CGI.unescapeHTML(text), text.html_safe).mark(LINK_PATTERN) do |link, left:, right:|
+ spaced_link_match(link)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/banzai/pipeline/wiki_pipeline.rb b/lib/banzai/pipeline/wiki_pipeline.rb
index c37b8e71cb0..737ff0cc818 100644
--- a/lib/banzai/pipeline/wiki_pipeline.rb
+++ b/lib/banzai/pipeline/wiki_pipeline.rb
@@ -5,6 +5,7 @@ module Banzai
@filters ||= begin
super.insert_after(Filter::TableOfContentsFilter, Filter::GollumTagsFilter)
.insert_before(Filter::TaskListFilter, Filter::WikiLinkFilter)
+ .insert_before(Filter::WikiLinkFilter, Filter::SpacedLinkFilter)
end
end
end
diff --git a/lib/gitlab/data_builder/build.rb b/lib/gitlab/data_builder/build.rb
index 2f1445a050a..0b71b31a476 100644
--- a/lib/gitlab/data_builder/build.rb
+++ b/lib/gitlab/data_builder/build.rb
@@ -28,6 +28,7 @@ module Gitlab
build_finished_at: build.finished_at,
build_duration: build.duration,
build_allow_failure: build.allow_failure,
+ build_failure_reason: build.failure_reason,
# TODO: do we still need it?
project_id: project.id,
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index b58296375ef..61ce10ca131 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -226,6 +226,7 @@ module Gitlab
@new_file = diff.from_id == BLANK_SHA
@renamed_file = diff.from_path != diff.to_path
@deleted_file = diff.to_id == BLANK_SHA
+ @too_large = diff.too_large if diff.respond_to?(:too_large)
collapse! if diff.respond_to?(:collapsed) && diff.collapsed
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/gitaly_client/diff.rb b/lib/gitlab/gitaly_client/diff.rb
index d98a0ce988f..af9d674535b 100644
--- a/lib/gitlab/gitaly_client/diff.rb
+++ b/lib/gitlab/gitaly_client/diff.rb
@@ -1,7 +1,7 @@
module Gitlab
module GitalyClient
class Diff
- ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed).freeze
+ ATTRS = %i(from_path to_path old_mode new_mode from_id to_id patch overflow_marker collapsed too_large).freeze
include AttributesBag
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/diff_note_importer.rb b/lib/gitlab/github_import/importer/diff_note_importer.rb
index 8274f37d358..d562958e955 100644
--- a/lib/gitlab/github_import/importer/diff_note_importer.rb
+++ b/lib/gitlab/github_import/importer/diff_note_importer.rb
@@ -13,7 +13,7 @@ module Gitlab
@note = note
@project = project
@client = client
- @user_finder = UserFinder.new(project, client)
+ @user_finder = GithubImport::UserFinder.new(project, client)
end
def execute
diff --git a/lib/gitlab/github_import/importer/issue_importer.rb b/lib/gitlab/github_import/importer/issue_importer.rb
index 31fefebf787..cb4d7a6a0b6 100644
--- a/lib/gitlab/github_import/importer/issue_importer.rb
+++ b/lib/gitlab/github_import/importer/issue_importer.rb
@@ -19,7 +19,7 @@ module Gitlab
@issue = issue
@project = project
@client = client
- @user_finder = UserFinder.new(project, client)
+ @user_finder = GithubImport::UserFinder.new(project, client)
@milestone_finder = MilestoneFinder.new(project)
@issuable_finder = GithubImport::IssuableFinder.new(project, issue)
end
@@ -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/note_importer.rb b/lib/gitlab/github_import/importer/note_importer.rb
index c890f2df360..2b06d1b3baf 100644
--- a/lib/gitlab/github_import/importer/note_importer.rb
+++ b/lib/gitlab/github_import/importer/note_importer.rb
@@ -13,7 +13,7 @@ module Gitlab
@note = note
@project = project
@client = client
- @user_finder = UserFinder.new(project, client)
+ @user_finder = GithubImport::UserFinder.new(project, client)
end
def execute
diff --git a/lib/gitlab/github_import/importer/pull_request_importer.rb b/lib/gitlab/github_import/importer/pull_request_importer.rb
index 6b3688c4381..ed17aa54373 100644
--- a/lib/gitlab/github_import/importer/pull_request_importer.rb
+++ b/lib/gitlab/github_import/importer/pull_request_importer.rb
@@ -15,7 +15,7 @@ module Gitlab
@pull_request = pull_request
@project = project
@client = client
- @user_finder = UserFinder.new(project, client)
+ @user_finder = GithubImport::UserFinder.new(project, client)
@milestone_finder = MilestoneFinder.new(project)
@issuable_finder =
GithubImport::IssuableFinder.new(project, pull_request)
@@ -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/hook_data/issuable_builder.rb b/lib/gitlab/hook_data/issuable_builder.rb
index f2eda398b8f..65389835ad7 100644
--- a/lib/gitlab/hook_data/issuable_builder.rb
+++ b/lib/gitlab/hook_data/issuable_builder.rb
@@ -28,7 +28,7 @@ module Gitlab
end
def safe_keys
- issuable_builder::SAFE_HOOK_ATTRIBUTES + issuable_builder::SAFE_HOOK_RELATIONS
+ issuable_builder.safe_hook_attributes + issuable_builder::SAFE_HOOK_RELATIONS
end
private
diff --git a/lib/gitlab/hook_data/issue_builder.rb b/lib/gitlab/hook_data/issue_builder.rb
index 0d71c748dc6..dd63db969f6 100644
--- a/lib/gitlab/hook_data/issue_builder.rb
+++ b/lib/gitlab/hook_data/issue_builder.rb
@@ -1,50 +1,52 @@
module Gitlab
module HookData
class IssueBuilder < BaseBuilder
- SAFE_HOOK_ATTRIBUTES = %i[
- assignee_id
- author_id
- closed_at
- confidential
- created_at
- description
- due_date
- id
- iid
- last_edited_at
- last_edited_by_id
- milestone_id
- moved_to_id
- project_id
- relative_position
- state
- time_estimate
- title
- updated_at
- updated_by_id
- ].freeze
-
SAFE_HOOK_RELATIONS = %i[
assignees
labels
total_time_spent
].freeze
+ def self.safe_hook_attributes
+ %i[
+ assignee_id
+ author_id
+ closed_at
+ confidential
+ created_at
+ description
+ due_date
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ milestone_id
+ moved_to_id
+ project_id
+ relative_position
+ state
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+ end
+
alias_method :issue, :object
def build
attrs = {
- description: absolute_image_urls(issue.description),
- url: Gitlab::UrlBuilder.build(issue),
- total_time_spent: issue.total_time_spent,
- human_total_time_spent: issue.human_total_time_spent,
- human_time_estimate: issue.human_time_estimate,
- assignee_ids: issue.assignee_ids,
- assignee_id: issue.assignee_ids.first # This key is deprecated
+ description: absolute_image_urls(issue.description),
+ url: Gitlab::UrlBuilder.build(issue),
+ total_time_spent: issue.total_time_spent,
+ human_total_time_spent: issue.human_total_time_spent,
+ human_time_estimate: issue.human_time_estimate,
+ assignee_ids: issue.assignee_ids,
+ assignee_id: issue.assignee_ids.first # This key is deprecated
}
- issue.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
- .merge!(attrs)
+ issue.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
+ .merge!(attrs)
end
end
end
diff --git a/lib/gitlab/hook_data/merge_request_builder.rb b/lib/gitlab/hook_data/merge_request_builder.rb
index dfbed0597ed..3aa6a4f3767 100644
--- a/lib/gitlab/hook_data/merge_request_builder.rb
+++ b/lib/gitlab/hook_data/merge_request_builder.rb
@@ -1,33 +1,35 @@
module Gitlab
module HookData
class MergeRequestBuilder < BaseBuilder
- SAFE_HOOK_ATTRIBUTES = %i[
- assignee_id
- author_id
- created_at
- description
- head_pipeline_id
- id
- iid
- last_edited_at
- last_edited_by_id
- merge_commit_sha
- merge_error
- merge_params
- merge_status
- merge_user_id
- merge_when_pipeline_succeeds
- milestone_id
- source_branch
- source_project_id
- state
- target_branch
- target_project_id
- time_estimate
- title
- updated_at
- updated_by_id
- ].freeze
+ def self.safe_hook_attributes
+ %i[
+ assignee_id
+ author_id
+ created_at
+ description
+ head_pipeline_id
+ id
+ iid
+ last_edited_at
+ last_edited_by_id
+ merge_commit_sha
+ merge_error
+ merge_params
+ merge_status
+ merge_user_id
+ merge_when_pipeline_succeeds
+ milestone_id
+ source_branch
+ source_project_id
+ state
+ target_branch
+ target_project_id
+ time_estimate
+ title
+ updated_at
+ updated_by_id
+ ].freeze
+ end
SAFE_HOOK_RELATIONS = %i[
assignee
@@ -50,8 +52,8 @@ module Gitlab
human_time_estimate: merge_request.human_time_estimate
}
- merge_request.attributes.with_indifferent_access.slice(*SAFE_HOOK_ATTRIBUTES)
- .merge!(attrs)
+ merge_request.attributes.with_indifferent_access.slice(*self.class.safe_hook_attributes)
+ .merge!(attrs)
end
end
end
diff --git a/lib/gitlab/import_export/members_mapper.rb b/lib/gitlab/import_export/members_mapper.rb
index ac827cbe1ca..bcbaf00e11b 100644
--- a/lib/gitlab/import_export/members_mapper.rb
+++ b/lib/gitlab/import_export/members_mapper.rb
@@ -45,7 +45,7 @@ module Gitlab
end
def ensure_default_member!
- @project.project_members.destroy_all
+ @project.project_members.destroy_all # rubocop: disable DestroyAll
ProjectMember.create!(user: @user, access_level: ProjectMember::MAINTAINER, source_id: @project.id, importing: true)
end
diff --git a/lib/tasks/gitlab/site_statistics.rake b/lib/tasks/gitlab/site_statistics.rake
new file mode 100644
index 00000000000..7d24ec72a9d
--- /dev/null
+++ b/lib/tasks/gitlab/site_statistics.rake
@@ -0,0 +1,23 @@
+namespace :gitlab do
+ desc "GitLab | Refresh Site Statistics counters"
+ task refresh_site_statistics: :environment do
+ puts 'Updating Site Statistics counters: '
+
+ print '* Repositories... '
+ SiteStatistic.transaction do
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+ ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql?
+ SiteStatistic.update_all('repositories_count = (SELECT COUNT(*) FROM projects)')
+ end
+ puts 'OK!'.color(:green)
+
+ print '* Wikis... '
+ SiteStatistic.transaction do
+ # see https://gitlab.com/gitlab-org/gitlab-ce/issues/48967
+ ActiveRecord::Base.connection.execute('SET LOCAL statement_timeout TO 0') if Gitlab::Database.postgresql?
+ SiteStatistic.update_all('wikis_count = (SELECT COUNT(*) FROM project_features WHERE wiki_access_level != 0)')
+ end
+ puts 'OK!'.color(:green)
+ puts
+ end
+end
diff --git a/locale/gitlab.pot b/locale/gitlab.pot
index b370cc13f11..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 ""
@@ -3181,6 +3247,9 @@ 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 ""
@@ -3262,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] ""
@@ -3321,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 ""
@@ -3357,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 ""
@@ -3483,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 ""
@@ -3531,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 ""
@@ -3549,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 ""
@@ -3719,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 ""
@@ -3764,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 ""
@@ -3875,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 ""
@@ -3983,6 +4089,9 @@ msgstr ""
msgid "Pending"
msgstr ""
+msgid "People without permission will never get a notification and won't be able to comment."
+msgstr ""
+
msgid "Per job. If a job passes this threshold, it will be marked as failed"
msgstr ""
@@ -4001,6 +4110,9 @@ msgstr ""
msgid "Pipeline"
msgstr ""
+msgid "Pipeline %{pipelineLinkStart} #%{pipelineId} %{pipelineLinkEnd} from %{pipelineLinkRefStart} %{pipelineRef} %{pipelineLinkRefEnd}"
+msgstr ""
+
msgid "Pipeline Health"
msgstr ""
@@ -4085,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 ""
@@ -4106,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 ""
@@ -4220,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 ""
@@ -4394,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 ""
@@ -4520,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 ""
@@ -4667,6 +4799,9 @@ msgstr ""
msgid "Retry verification"
msgstr ""
+msgid "Reveal Variables"
+msgstr ""
+
msgid "Reveal value"
msgid_plural "Reveal values"
msgstr[0] ""
@@ -4983,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 ""
@@ -5321,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 ""
@@ -5351,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 ""
@@ -5453,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 ""
@@ -5504,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 ""
@@ -5513,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 ""
@@ -5528,6 +5687,9 @@ msgstr ""
msgid "This project does not belong to a group and can therefore not make use of group Runners."
msgstr ""
+msgid "This project does not have billing enabled. To create a cluster, <a href=%{linkToBilling} target=\"_blank\" rel=\"noopener noreferrer\">enable billing <i class=\"fa fa-external-link\" aria-hidden=\"true\"></i></a> and try again."
+msgstr ""
+
msgid "This repository"
msgstr ""
@@ -5802,6 +5964,9 @@ msgstr ""
msgid "Trending"
msgstr ""
+msgid "Trigger"
+msgstr ""
+
msgid "Trigger this manual action"
msgstr ""
@@ -5820,6 +5985,9 @@ msgstr ""
msgid "Unlock"
msgstr ""
+msgid "Unlock this %{issuableDisplayName}? <strong>Everyone</strong> will be able to comment."
+msgstr ""
+
msgid "Unlocked"
msgstr ""
@@ -6333,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 ""
@@ -6436,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 ""
@@ -6463,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 ""
@@ -6487,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 ""
@@ -6505,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/qa/README.md b/qa/README.md
index be4cf89ebbc..f8a5c00efd4 100644
--- a/qa/README.md
+++ b/qa/README.md
@@ -35,7 +35,7 @@ following call would login to a local [GDK] instance and run all specs in
`qa/specs/features`:
```
-bin/qa Test::Instance http://localhost:3000
+bin/qa Test::Instance::All http://localhost:3000
```
### Writing tests
@@ -48,14 +48,14 @@ You can also supply specific tests to run as another parameter. For example, to
run the repository-related specs, you can execute:
```
-bin/qa Test::Instance http://localhost qa/specs/features/repository/
+bin/qa Test::Instance::All http://localhost qa/specs/features/repository/
```
Since the arguments would be passed to `rspec`, you could use all `rspec`
options there. For example, passing `--backtrace` and also line number:
```
-bin/qa Test::Instance http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace
+bin/qa Test::Instance::All http://localhost qa/specs/features/project/create_spec.rb:3 --backtrace
```
### Overriding the authenticated user
@@ -67,7 +67,7 @@ If you need to authenticate as a different user, you can provide the
`GITLAB_USERNAME` and `GITLAB_PASSWORD` environment variables:
```
-GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance https://gitlab.example.com
+GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password bin/qa Test::Instance::All https://gitlab.example.com
```
If your user doesn't have permission to default sandbox group
@@ -75,13 +75,13 @@ If your user doesn't have permission to default sandbox group
`GITLAB_SANDBOX_NAME`:
```
-GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com
+GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com
```
In addition, the `GITLAB_USER_TYPE` can be set to "ldap" to sign in as an LDAP user:
```
-GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance https://gitlab.example.com
+GITLAB_USER_TYPE=ldap GITLAB_USERNAME=jsmith GITLAB_PASSWORD=password GITLAB_SANDBOX_NAME=jsmith-qa-sandbox bin/qa Test::Instance::All https://gitlab.example.com
```
All [supported environment variables are here](https://gitlab.com/gitlab-org/gitlab-qa#supported-environment-variables).
diff --git a/qa/qa.rb b/qa/qa.rb
index 0b48cf58766..4b927067449 100644
--- a/qa/qa.rb
+++ b/qa/qa.rb
@@ -77,14 +77,16 @@ module QA
#
autoload :Bootable, 'qa/scenario/bootable'
autoload :Actable, 'qa/scenario/actable'
- autoload :Taggable, 'qa/scenario/taggable'
autoload :Template, 'qa/scenario/template'
##
# Test scenario entrypoints.
#
module Test
- autoload :Instance, 'qa/scenario/test/instance'
+ module Instance
+ autoload :All, 'qa/scenario/test/instance/all'
+ autoload :Smoke, 'qa/scenario/test/instance/smoke'
+ end
module Integration
autoload :Github, 'qa/scenario/test/integration/github'
diff --git a/qa/qa/factory/resource/fork.rb b/qa/qa/factory/resource/fork.rb
index 1d0c76a3d30..01969c31438 100644
--- a/qa/qa/factory/resource/fork.rb
+++ b/qa/qa/factory/resource/fork.rb
@@ -4,7 +4,12 @@ module QA
class Fork < Factory::Base
dependency Factory::Repository::ProjectPush, as: :push
- dependency Factory::Resource::User, as: :user
+ dependency Factory::Resource::User, as: :user do |user|
+ if Runtime::Env.forker?
+ user.username = Runtime::Env.forker_username
+ user.password = Runtime::Env.forker_password
+ end
+ end
product(:user) { |factory| factory.user }
diff --git a/qa/qa/factory/resource/user.rb b/qa/qa/factory/resource/user.rb
index e08df9e0cd0..eac2a873bd5 100644
--- a/qa/qa/factory/resource/user.rb
+++ b/qa/qa/factory/resource/user.rb
@@ -4,28 +4,52 @@ module QA
module Factory
module Resource
class User < Factory::Base
- attr_accessor :name, :username, :email, :password
+ attr_reader :unique_id
+ attr_writer :username, :password, :name, :email
def initialize
- @name = "name-#{SecureRandom.hex(8)}"
- @username = "username-#{SecureRandom.hex(8)}"
- @email = "mail#{SecureRandom.hex(8)}@mail.com"
- @password = 'password'
+ @unique_id = SecureRandom.hex(8)
end
- product(:name) { |factory| factory.name }
+ def username
+ @username ||= "qa-user-#{unique_id}"
+ end
- product(:username) { |factory| factory.username }
+ def password
+ @password ||= 'password'
+ end
- product(:email) { |factory| factory.email }
+ def name
+ @name ||= username
+ end
+
+ def email
+ @email ||= "#{username}@example.com"
+ end
+
+ def credentials_given?
+ defined?(@username) && defined?(@password)
+ end
+ product(:name) { |factory| factory.name }
+ product(:username) { |factory| factory.username }
+ product(:email) { |factory| factory.email }
product(:password) { |factory| factory.password }
def fabricate!
- Page::Menu::Main.act { sign_out }
- Page::Main::Login.act { switch_to_register_tab }
- Page::Main::SignUp.perform do |page|
- page.sign_up!(name: name, username: username, email: email, password: password)
+ Page::Menu::Main.perform { |main| main.sign_out }
+
+ if credentials_given?
+ Page::Main::Login.perform do |login|
+ login.sign_in_using_credentials(self)
+ end
+ else
+ Page::Main::Login.perform do |login|
+ login.switch_to_register_tab
+ end
+ Page::Main::SignUp.perform do |signup|
+ signup.sign_up!(self)
+ end
end
end
end
diff --git a/qa/qa/git/repository.rb b/qa/qa/git/repository.rb
index 3df6db05970..bdbb18b5045 100644
--- a/qa/qa/git/repository.rb
+++ b/qa/qa/git/repository.rb
@@ -28,7 +28,7 @@ module QA
end
def use_default_credentials
- self.username = Runtime::User.name
+ self.username = Runtime::User.username
self.password = Runtime::User.password
end
diff --git a/qa/qa/page/main/login.rb b/qa/qa/page/main/login.rb
index 6cdfbd1c125..afc8b66d878 100644
--- a/qa/qa/page/main/login.rb
+++ b/qa/qa/page/main/login.rb
@@ -40,17 +40,19 @@ module QA
end
end
- def sign_in_using_credentials
+ def sign_in_using_credentials(user = nil)
# Don't try to log-in if we're already logged-in
return if Page::Menu::Main.act { has_personal_area?(wait: 0) }
using_wait_time 0 do
set_initial_password_if_present
+ raise NotImplementedError if Runtime::User.ldap_user? && user&.credentials_given?
+
if Runtime::User.ldap_user?
sign_in_using_ldap_credentials
else
- sign_in_using_gitlab_credentials
+ sign_in_using_gitlab_credentials(user || Runtime::User)
end
end
@@ -69,21 +71,30 @@ module QA
click_on 'Register'
end
+ def switch_to_ldap_tab
+ click_on 'LDAP'
+ end
+
+ def switch_to_standard_tab
+ click_on 'Standard'
+ end
+
private
def sign_in_using_ldap_credentials
- click_link 'LDAP'
+ switch_to_ldap_tab
fill_in :username, with: Runtime::User.ldap_username
fill_in :password, with: Runtime::User.ldap_password
click_button 'Sign in'
end
- def sign_in_using_gitlab_credentials
- click_link 'Standard' if page.has_content?('LDAP')
+ def sign_in_using_gitlab_credentials(user)
+ switch_to_sign_in_tab unless page.has_button?('Sign in')
+ switch_to_standard_tab if page.has_content?('LDAP')
- fill_in :user_login, with: Runtime::User.name
- fill_in :user_password, with: Runtime::User.password
+ fill_in :user_login, with: user.username
+ fill_in :user_password, with: user.password
click_button 'Sign in'
end
diff --git a/qa/qa/page/main/sign_up.rb b/qa/qa/page/main/sign_up.rb
index 9a834e94b81..33ab56236f4 100644
--- a/qa/qa/page/main/sign_up.rb
+++ b/qa/qa/page/main/sign_up.rb
@@ -11,12 +11,12 @@ module QA
element :register_button, 'submit "Register"'
end
- def sign_up!(name:, username:, email:, password:)
- fill_in :new_user_name, with: name
- fill_in :new_user_username, with: username
- fill_in :new_user_email, with: email
- fill_in :new_user_email_confirmation, with: email
- fill_in :new_user_password, with: password
+ def sign_up!(user)
+ fill_in :new_user_name, with: user.name
+ fill_in :new_user_username, with: user.username
+ fill_in :new_user_email, with: user.email
+ fill_in :new_user_email_confirmation, with: user.email
+ fill_in :new_user_password, with: user.password
click_button 'Register'
Page::Menu::Main.act { has_personal_area? }
diff --git a/qa/qa/runtime/env.rb b/qa/qa/runtime/env.rb
index 5dc194e0aef..841c959045f 100644
--- a/qa/qa/runtime/env.rb
+++ b/qa/qa/runtime/env.rb
@@ -39,6 +39,18 @@ module QA
ENV['GITLAB_PASSWORD']
end
+ def forker?
+ forker_username && forker_password
+ end
+
+ def forker_username
+ ENV['GITLAB_FORKER_USERNAME']
+ end
+
+ def forker_password
+ ENV['GITLAB_FORKER_PASSWORD']
+ end
+
def ldap_username
ENV['GITLAB_LDAP_USERNAME']
end
diff --git a/qa/qa/runtime/user.rb b/qa/qa/runtime/user.rb
index c80ee6d4d96..b016777c987 100644
--- a/qa/qa/runtime/user.rb
+++ b/qa/qa/runtime/user.rb
@@ -3,12 +3,12 @@ module QA
module User
extend self
- def default_name
+ def default_username
'root'
end
- def name
- Runtime::Env.user_username || default_name
+ def username
+ Runtime::Env.user_username || default_username
end
def password
diff --git a/qa/qa/scenario/taggable.rb b/qa/qa/scenario/taggable.rb
deleted file mode 100644
index b1f24d742e0..00000000000
--- a/qa/qa/scenario/taggable.rb
+++ /dev/null
@@ -1,17 +0,0 @@
-module QA
- module Scenario
- module Taggable
- # rubocop:disable Gitlab/ModuleWithInstanceVariables
-
- def tags(*tags)
- @tags = tags
- end
-
- def focus
- @tags.to_a
- end
-
- # rubocop:enable Gitlab/ModuleWithInstanceVariables
- end
- end
-end
diff --git a/qa/qa/scenario/template.rb b/qa/qa/scenario/template.rb
index d21a9d52997..66eb86f25c8 100644
--- a/qa/qa/scenario/template.rb
+++ b/qa/qa/scenario/template.rb
@@ -1,15 +1,36 @@
module QA
module Scenario
class Template
- def self.perform(*args)
- new.tap do |scenario|
- yield scenario if block_given?
- break scenario.perform(*args)
+ class << self
+ def perform(*args)
+ new.tap do |scenario|
+ yield scenario if block_given?
+ break scenario.perform(*args)
+ end
+ end
+
+ def tags(*tags)
+ @tags = tags
+ end
+
+ def focus
+ @tags.to_a
end
end
- def perform(*_args)
- raise NotImplementedError
+ def perform(address, *rspec_options)
+ Runtime::Scenario.define(:gitlab_address, address)
+
+ Specs::Runner.perform do |specs|
+ specs.tty = true
+ specs.tags = self.class.focus
+ specs.options =
+ if rspec_options.any?
+ rspec_options
+ else
+ ::File.expand_path('../specs/features', __dir__)
+ end
+ end
end
end
end
diff --git a/qa/qa/scenario/test/instance.rb b/qa/qa/scenario/test/instance.rb
deleted file mode 100644
index 46eb2dabb11..00000000000
--- a/qa/qa/scenario/test/instance.rb
+++ /dev/null
@@ -1,36 +0,0 @@
-module QA
- module Scenario
- module Test
- ##
- # Base class for running the suite against any GitLab instance,
- # including staging and on-premises installation.
- #
- class Instance < Template
- include Bootable
- extend Taggable
-
- tags :core
-
- def perform(address, *rspec_options)
- Runtime::Scenario.define(:gitlab_address, address)
-
- ##
- # Perform before hooks, which are different for CE and EE
- #
- Runtime::Release.perform_before_hooks
-
- Specs::Runner.perform do |specs|
- specs.tty = true
- specs.tags = self.class.focus
- specs.options =
- if rspec_options.any?
- rspec_options
- else
- ::File.expand_path('../../specs/features', __dir__)
- end
- end
- end
- end
- end
- end
-end
diff --git a/qa/qa/scenario/test/instance/all.rb b/qa/qa/scenario/test/instance/all.rb
new file mode 100644
index 00000000000..a07c26431bd
--- /dev/null
+++ b/qa/qa/scenario/test/instance/all.rb
@@ -0,0 +1,15 @@
+module QA
+ module Scenario
+ module Test
+ ##
+ # Base class for running the suite against any GitLab instance,
+ # including staging and on-premises installation.
+ #
+ module Instance
+ class All < Template
+ include Bootable
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/scenario/test/instance/smoke.rb b/qa/qa/scenario/test/instance/smoke.rb
new file mode 100644
index 00000000000..a7d2cb27f27
--- /dev/null
+++ b/qa/qa/scenario/test/instance/smoke.rb
@@ -0,0 +1,17 @@
+module QA
+ module Scenario
+ module Test
+ module Instance
+ ##
+ # Base class for running the suite against any GitLab instance,
+ # including staging and on-premises installation.
+ #
+ class Smoke < Template
+ include Bootable
+
+ tags :smoke
+ end
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/api/basics_spec.rb b/qa/qa/specs/features/api/basics_spec.rb
index 6563b56d1b4..bc0b5ebfe10 100644
--- a/qa/qa/specs/features/api/basics_spec.rb
+++ b/qa/qa/specs/features/api/basics_spec.rb
@@ -1,13 +1,13 @@
require 'securerandom'
module QA
- describe 'API basics', :core do
+ describe 'API basics' do
before(:context) do
@api_client = Runtime::API::Client.new(:gitlab)
end
let(:project_name) { "api-basics-#{SecureRandom.hex(8)}" }
- let(:sanitized_project_path) { CGI.escape("#{Runtime::User.name}/#{project_name}") }
+ let(:sanitized_project_path) { CGI.escape("#{Runtime::User.username}/#{project_name}") }
it 'user creates a project with a file and deletes them afterwards' do
create_project_request = Runtime::API::Request.new(@api_client, '/projects')
diff --git a/qa/qa/specs/features/api/users_spec.rb b/qa/qa/specs/features/api/users_spec.rb
index 8a63d8095c9..3d25cca1e59 100644
--- a/qa/qa/specs/features/api/users_spec.rb
+++ b/qa/qa/specs/features/api/users_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'API users', :core do
+ describe 'API users' do
before(:context) do
@api_client = Runtime::API::Client.new(:gitlab)
end
@@ -14,11 +14,11 @@ module QA
end
it 'submit request with a valid user name' do
- get request.url, { params: { username: Runtime::User.name } }
+ get request.url, { params: { username: Runtime::User.username } }
expect_status(200)
expect(json_body).to contain_exactly(
- a_hash_including(username: Runtime::User.name)
+ a_hash_including(username: Runtime::User.username)
)
end
diff --git a/qa/qa/specs/features/login/basic_spec.rb b/qa/qa/specs/features/login/basic_spec.rb
new file mode 100644
index 00000000000..f866466c7bf
--- /dev/null
+++ b/qa/qa/specs/features/login/basic_spec.rb
@@ -0,0 +1,15 @@
+module QA
+ describe 'basic user login', :smoke do
+ it 'user logs in using basic credentials' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ # TODO, since `Signed in successfully` message was removed
+ # this is the only way to tell if user is signed in correctly.
+ #
+ Page::Menu::Main.perform do |menu|
+ expect(menu).to have_personal_area
+ end
+ end
+ end
+end
diff --git a/qa/qa/specs/features/login/ldap_spec.rb b/qa/qa/specs/features/login/ldap_spec.rb
index b7a284c584b..de6111eea64 100644
--- a/qa/qa/specs/features/login/ldap_spec.rb
+++ b/qa/qa/specs/features/login/ldap_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'LDAP user login', :ldap do
+ describe 'LDAP user login', :orchestrated, :ldap do
before do
Runtime::Env.user_type = 'ldap'
end
diff --git a/qa/qa/specs/features/mattermost/group_create_spec.rb b/qa/qa/specs/features/mattermost/group_create_spec.rb
index a59761da341..097e1713aef 100644
--- a/qa/qa/specs/features/mattermost/group_create_spec.rb
+++ b/qa/qa/specs/features/mattermost/group_create_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'create a new group', :mattermost do
+ describe 'create a new group', :orchestrated, :mattermost do
it 'creating a group with a mattermost team' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/mattermost/login_spec.rb b/qa/qa/specs/features/mattermost/login_spec.rb
index b140191e160..27f7d4c245f 100644
--- a/qa/qa/specs/features/mattermost/login_spec.rb
+++ b/qa/qa/specs/features/mattermost/login_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'logging in to Mattermost', :mattermost do
+ describe 'logging in to Mattermost', :orchestrated, :mattermost do
it 'can use gitlab oauth' do
Runtime::Browser.visit(:gitlab, Page::Main::Login) do
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/merge_request/create_spec.rb b/qa/qa/specs/features/merge_request/create_spec.rb
index 36d7efb02e1..71e79956b85 100644
--- a/qa/qa/specs/features/merge_request/create_spec.rb
+++ b/qa/qa/specs/features/merge_request/create_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'creates a merge request', :core do
+ describe 'creates a merge request with milestone' do
it 'user creates a new merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -29,4 +29,25 @@ module QA
end
end
end
+
+ describe 'creates a merge request', :smoke do
+ it 'user creates a new merge request' do
+ Runtime::Browser.visit(:gitlab, Page::Main::Login)
+ Page::Main::Login.act { sign_in_using_credentials }
+
+ current_project = Factory::Resource::Project.fabricate! do |project|
+ project.name = 'project-with-merge-request'
+ end
+
+ Factory::Resource::MergeRequest.fabricate! do |merge_request|
+ merge_request.title = 'This is a merge request'
+ merge_request.description = 'Great feature'
+ merge_request.project = current_project
+ end
+
+ expect(page).to have_content('This is a merge request')
+ expect(page).to have_content('Great feature')
+ expect(page).to have_content(/Opened [\w\s]+ ago/)
+ end
+ end
end
diff --git a/qa/qa/specs/features/merge_request/rebase_spec.rb b/qa/qa/specs/features/merge_request/rebase_spec.rb
index 163dcbe7963..c36d28e4237 100644
--- a/qa/qa/specs/features/merge_request/rebase_spec.rb
+++ b/qa/qa/specs/features/merge_request/rebase_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'merge request rebase', :core do
+ describe 'merge request rebase' do
it 'rebases source branch of merge request' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/merge_request/squash_spec.rb b/qa/qa/specs/features/merge_request/squash_spec.rb
index 4856bbe1a69..3ecc36a5ae1 100644
--- a/qa/qa/specs/features/merge_request/squash_spec.rb
+++ b/qa/qa/specs/features/merge_request/squash_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'merge request squash commits', :core do
+ describe 'merge request squash commits' do
it 'when squash commits is marked before merge' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/activity_spec.rb b/qa/qa/specs/features/project/activity_spec.rb
index 02074e386b6..c7ce8dfdcc6 100644
--- a/qa/qa/specs/features/project/activity_spec.rb
+++ b/qa/qa/specs/features/project/activity_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'activity page', :core do
+ describe 'activity page' do
it 'push creates an event in the activity page' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/add_deploy_key_spec.rb b/qa/qa/specs/features/project/add_deploy_key_spec.rb
index 14642af97ad..24f9f4c77f8 100644
--- a/qa/qa/specs/features/project/add_deploy_key_spec.rb
+++ b/qa/qa/specs/features/project/add_deploy_key_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'deploy keys support', :core do
+ describe 'deploy keys support' do
it 'user adds a deploy key' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/add_secret_variable_spec.rb b/qa/qa/specs/features/project/add_secret_variable_spec.rb
index 32c91dd9d4d..04d9fe488e2 100644
--- a/qa/qa/specs/features/project/add_secret_variable_spec.rb
+++ b/qa/qa/specs/features/project/add_secret_variable_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'secret variables support', :core do
+ describe 'secret variables support' do
it 'user adds a secret variable' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/auto_devops_spec.rb b/qa/qa/specs/features/project/auto_devops_spec.rb
index c2c3bef98e4..248669b6046 100644
--- a/qa/qa/specs/features/project/auto_devops_spec.rb
+++ b/qa/qa/specs/features/project/auto_devops_spec.rb
@@ -1,7 +1,7 @@
require 'pathname'
module QA
- describe 'Auto Devops', :kubernetes do
+ describe 'Auto Devops', :orchestrated, :kubernetes do
after do
@cluster&.remove!
end
@@ -15,6 +15,13 @@ module QA
p.description = 'Project with Auto Devops'
end
+ # Disable code_quality check in Auto DevOps pipeline as it takes
+ # too long and times out the test
+ Factory::Resource::SecretVariable.fabricate! do |resource|
+ resource.key = 'CODE_QUALITY_DISABLED'
+ resource.value = '1'
+ end
+
# Create Auto Devops compatible repo
Factory::Repository::ProjectPush.fabricate! do |push|
push.project = project
diff --git a/qa/qa/specs/features/project/create_issue_spec.rb b/qa/qa/specs/features/project/create_issue_spec.rb
index ac2ed2ca2a1..793e7db87cb 100644
--- a/qa/qa/specs/features/project/create_issue_spec.rb
+++ b/qa/qa/specs/features/project/create_issue_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'creates issue', :core do
+ describe 'creates issue', :smoke do
let(:issue_title) { 'issue title' }
it 'user creates issue' do
diff --git a/qa/qa/specs/features/project/create_spec.rb b/qa/qa/specs/features/project/create_spec.rb
index 14ecd464685..5e19e490778 100644
--- a/qa/qa/specs/features/project/create_spec.rb
+++ b/qa/qa/specs/features/project/create_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'create a new project', :core do
+ describe 'create a new project', :smoke do
it 'user creates a new project' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/deploy_key_clone_spec.rb b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
index 054f49b408a..1d099508c24 100644
--- a/qa/qa/specs/features/project/deploy_key_clone_spec.rb
+++ b/qa/qa/specs/features/project/deploy_key_clone_spec.rb
@@ -1,7 +1,7 @@
require 'digest/sha1'
module QA
- describe 'cloning code using a deploy key', :core, :docker do
+ describe 'cloning code using a deploy key', :docker do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/project/fork_project_spec.rb b/qa/qa/specs/features/project/fork_project_spec.rb
index 8ad0120305a..280978bb950 100644
--- a/qa/qa/specs/features/project/fork_project_spec.rb
+++ b/qa/qa/specs/features/project/fork_project_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'Project fork', :core do
+ describe 'Project fork' do
it 'can submit merge requests to upstream master' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
@@ -8,14 +8,12 @@ module QA
merge_request.fork_branch = 'feature-branch'
end
- Page::Menu::Main.act { sign_out }
- Page::Main::Login.act do
- switch_to_sign_in_tab
- sign_in_using_credentials
- end
+ Page::Menu::Main.perform { |main| main.sign_out }
+ Page::Main::Login.perform { |login| login.sign_in_using_credentials }
merge_request.visit!
- Page::MergeRequest::Show.act { merge! }
+
+ Page::MergeRequest::Show.perform { |show| show.merge! }
expect(page).to have_content('The changes were merged')
end
diff --git a/qa/qa/specs/features/project/import_from_github_spec.rb b/qa/qa/specs/features/project/import_from_github_spec.rb
index 221b5c27fba..57695d2c726 100644
--- a/qa/qa/specs/features/project/import_from_github_spec.rb
+++ b/qa/qa/specs/features/project/import_from_github_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'user imports a GitHub repo', :core, :github do
+ describe 'user imports a GitHub repo', :orchestrated, :github do
let(:imported_project) do
Factory::Resource::ProjectImportedFromGithub.fabricate! do |project|
project.name = 'imported-project'
diff --git a/qa/qa/specs/features/project/pipelines_spec.rb b/qa/qa/specs/features/project/pipelines_spec.rb
index ddedde7a8bc..6c6b4e80626 100644
--- a/qa/qa/specs/features/project/pipelines_spec.rb
+++ b/qa/qa/specs/features/project/pipelines_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'CI/CD Pipelines', :core, :docker do
+ describe 'CI/CD Pipelines', :orchestrated, :docker do
let(:executor) { "qa-runner-#{Time.now.to_i}" }
after do
diff --git a/qa/qa/specs/features/project/wikis_spec.rb b/qa/qa/specs/features/project/wikis_spec.rb
index 59cc455fffc..9af2dbd1264 100644
--- a/qa/qa/specs/features/project/wikis_spec.rb
+++ b/qa/qa/specs/features/project/wikis_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'Wiki Functionality', :core do
+ describe 'Wiki Functionality' do
def login
Runtime::Browser.visit(:gitlab, Page::Main::Login)
Page::Main::Login.act { sign_in_using_credentials }
diff --git a/qa/qa/specs/features/repository/clone_spec.rb b/qa/qa/specs/features/repository/clone_spec.rb
index a04ce4e44d9..8b0613c5f78 100644
--- a/qa/qa/specs/features/repository/clone_spec.rb
+++ b/qa/qa/specs/features/repository/clone_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'clone code from the repository', :core do
+ describe 'clone code from the repository' do
context 'with regular account over http' do
let(:location) do
Page::Project::Show.act do
diff --git a/qa/qa/specs/features/repository/protected_branches_spec.rb b/qa/qa/specs/features/repository/protected_branches_spec.rb
index c2de94516d9..aa23145478d 100644
--- a/qa/qa/specs/features/repository/protected_branches_spec.rb
+++ b/qa/qa/specs/features/repository/protected_branches_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'branch protection support', :core do
+ describe 'branch protection support' do
let(:branch_name) { 'protected-branch' }
let(:commit_message) { 'Protected push commit message' }
let(:project) do
diff --git a/qa/qa/specs/features/repository/push_spec.rb b/qa/qa/specs/features/repository/push_spec.rb
index fc40b60d915..1e89942e932 100644
--- a/qa/qa/specs/features/repository/push_spec.rb
+++ b/qa/qa/specs/features/repository/push_spec.rb
@@ -1,5 +1,5 @@
module QA
- describe 'push code to repository', :core do
+ describe 'push code to repository' do
context 'with regular account over http' do
it 'user pushes code to the repository' do
Runtime::Browser.visit(:gitlab, Page::Main::Login)
diff --git a/qa/qa/specs/runner.rb b/qa/qa/specs/runner.rb
index f8f6fe65599..ccb9d5591de 100644
--- a/qa/qa/specs/runner.rb
+++ b/qa/qa/specs/runner.rb
@@ -14,7 +14,13 @@ module QA
def perform
args = []
args.push('--tty') if tty
- tags.to_a.each { |tag| args.push(['-t', tag.to_s]) }
+
+ if tags.any?
+ tags.each { |tag| args.push(['-t', tag.to_s]) }
+ else
+ args.push(%w[-t ~orchestrated])
+ end
+
args.push(options)
Runtime::Browser.configure!
diff --git a/qa/spec/runtime/env_spec.rb b/qa/spec/runtime/env_spec.rb
index 851026c71f0..ccc0b906845 100644
--- a/qa/spec/runtime/env_spec.rb
+++ b/qa/spec/runtime/env_spec.rb
@@ -77,6 +77,31 @@ describe QA::Runtime::Env do
end
end
+ describe '.forker?' do
+ it 'returns false if no forker credentials are defined' do
+ expect(described_class).not_to be_forker
+ end
+
+ it 'returns false if only forker username is defined' do
+ stub_env('GITLAB_FORKER_USERNAME', 'foo')
+
+ expect(described_class).not_to be_forker
+ end
+
+ it 'returns false if only forker password is defined' do
+ stub_env('GITLAB_FORKER_PASSWORD', 'bar')
+
+ expect(described_class).not_to be_forker
+ end
+
+ it 'returns true if forker username and password are defined' do
+ stub_env('GITLAB_FORKER_USERNAME', 'foo')
+ stub_env('GITLAB_FORKER_PASSWORD', 'bar')
+
+ expect(described_class).to be_forker
+ end
+ end
+
describe '.github_access_token' do
it 'returns "" if GITHUB_ACCESS_TOKEN is not defined' do
expect(described_class.github_access_token).to eq('')
diff --git a/qa/spec/scenario/test/instance_spec.rb b/qa/spec/scenario/test/instance/all_spec.rb
index 0d0b534911f..423527e938e 100644
--- a/qa/spec/scenario/test/instance_spec.rb
+++ b/qa/spec/scenario/test/instance/all_spec.rb
@@ -1,10 +1,4 @@
-describe QA::Scenario::Test::Instance do
- subject do
- Class.new(described_class) do
- tags :rspec
- end
- end
-
+describe QA::Scenario::Test::Instance::All do
context '#perform' do
let(:arguments) { spy('Runtime::Scenario') }
let(:release) { spy('Runtime::Release') }
@@ -26,16 +20,16 @@ describe QA::Scenario::Test::Instance do
end
context 'no paths' do
- it 'should call runner with default arguments' do
+ it 'calls runner with default arguments' do
subject.perform("test")
expect(runner).to have_received(:options=)
- .with(::File.expand_path('../../../qa/specs/features', __dir__))
+ .with(::File.expand_path('../../../../qa/specs/features', __dir__))
end
end
context 'specifying paths' do
- it 'should call runner with paths' do
+ it 'calls runner with paths' do
subject.perform('test', 'path1', 'path2')
expect(runner).to have_received(:options=).with(%w[path1 path2])
diff --git a/qa/spec/scenario/test/instance/smoke_spec.rb b/qa/spec/scenario/test/instance/smoke_spec.rb
new file mode 100644
index 00000000000..e79d19e8212
--- /dev/null
+++ b/qa/spec/scenario/test/instance/smoke_spec.rb
@@ -0,0 +1,45 @@
+describe QA::Scenario::Test::Instance::Smoke do
+ subject { Class.new(described_class) { tags :smoke } }
+
+ context '#perform' do
+ let(:arguments) { spy('Runtime::Scenario') }
+ let(:release) { spy('Runtime::Release') }
+ let(:runner) { spy('Specs::Runner') }
+
+ before do
+ stub_const('QA::Runtime::Release', release)
+ stub_const('QA::Runtime::Scenario', arguments)
+ stub_const('QA::Specs::Runner', runner)
+
+ allow(runner).to receive(:perform).and_yield(runner)
+ end
+
+ it 'sets an address of the subject' do
+ subject.perform("hello")
+
+ expect(arguments).to have_received(:define)
+ .with(:gitlab_address, "hello")
+ end
+
+ it 'has a smoke tag' do
+ expect(subject.focus).to eq([:smoke]) # rubocop:disable Focus
+ end
+
+ context 'no paths' do
+ it 'calls runner with default arguments' do
+ subject.perform("test")
+
+ expect(runner).to have_received(:options=)
+ .with(::File.expand_path('../../../../qa/specs/features', __dir__))
+ end
+ end
+
+ context 'specifying paths' do
+ it 'calls runner with paths' do
+ subject.perform('test', 'path1', 'path2')
+
+ expect(runner).to have_received(:options=).with(%w[path1 path2])
+ end
+ end
+ end
+end
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/autocomplete_controller_spec.rb b/spec/controllers/autocomplete_controller_spec.rb
index 2c59d1929a1..883bb35f396 100644
--- a/spec/controllers/autocomplete_controller_spec.rb
+++ b/spec/controllers/autocomplete_controller_spec.rb
@@ -274,14 +274,11 @@ describe AutocompleteController do
context 'authorized projects apply limit' do
before do
- authorized_project2 = create(:project)
- authorized_project3 = create(:project)
-
- authorized_project.add_maintainer(user)
- authorized_project2.add_maintainer(user)
- authorized_project3.add_maintainer(user)
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
- stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+ create_list(:project, 2) do |project|
+ project.add_maintainer(user)
+ end
end
describe 'GET #projects with project ID' do
@@ -291,7 +288,7 @@ describe AutocompleteController do
it 'returns projects' do
expect(json_response).to be_kind_of(Array)
- expect(json_response.size).to eq 2 # Of a total of 3
+ expect(json_response.size).to eq(Kaminari.config.default_per_page)
end
end
end
diff --git a/spec/controllers/omniauth_callbacks_controller_spec.rb b/spec/controllers/omniauth_callbacks_controller_spec.rb
index b23f183fec8..d377d69457f 100644
--- a/spec/controllers/omniauth_callbacks_controller_spec.rb
+++ b/spec/controllers/omniauth_callbacks_controller_spec.rb
@@ -95,7 +95,7 @@ describe OmniauthCallbacksController, type: :controller do
end
it 'allows linking the disabled provider' do
- user.identities.destroy_all
+ user.identities.destroy_all # rubocop: disable DestroyAll
sign_in(user)
expect { post provider }.to change { user.reload.identities.count }.by(1)
diff --git a/spec/controllers/projects/releases_controller_spec.rb b/spec/controllers/projects/releases_controller_spec.rb
index fc1619acec6..20a6beb3df8 100644
--- a/spec/controllers/projects/releases_controller_spec.rb
+++ b/spec/controllers/projects/releases_controller_spec.rb
@@ -14,7 +14,7 @@ describe Projects::ReleasesController do
describe 'GET #edit' do
it 'initializes a new release' do
tag_id = release.tag
- project.releases.destroy_all
+ project.releases.destroy_all # rubocop: disable DestroyAll
get :edit, namespace_id: project.namespace, project_id: project, tag_id: tag_id
diff --git a/spec/features/admin/admin_runners_spec.rb b/spec/features/admin/admin_runners_spec.rb
index be8754a5315..5623e47eadf 100644
--- a/spec/features/admin/admin_runners_spec.rb
+++ b/spec/features/admin/admin_runners_spec.rb
@@ -20,7 +20,7 @@ describe "Admin Runners" do
it 'has all necessary texts' do
expect(page).to have_text "Setup a shared Runner manually"
- expect(page).to have_text "Runners with last contact more than a minute ago: 1"
+ expect(page).to have_text "Runners currently online: 1"
end
describe 'search' do
@@ -55,7 +55,7 @@ describe "Admin Runners" do
it 'has all necessary texts including no runner message' do
expect(page).to have_text "Setup a shared Runner manually"
- expect(page).to have_text "Runners with last contact more than a minute ago: 0"
+ expect(page).to have_text "Runners currently online: 0"
expect(page).to have_text 'No runners found'
end
end
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/finders/autocomplete/group_finder_spec.rb b/spec/finders/autocomplete/group_finder_spec.rb
new file mode 100644
index 00000000000..d7cb2c3bbe2
--- /dev/null
+++ b/spec/finders/autocomplete/group_finder_spec.rb
@@ -0,0 +1,58 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Autocomplete::GroupFinder do
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ context 'with a project' do
+ it 'returns nil' do
+ project = create(:project)
+
+ expect(described_class.new(user, project).execute).to be_nil
+ end
+ end
+
+ context 'without a group ID' do
+ it 'returns nil' do
+ expect(described_class.new(user).execute).to be_nil
+ end
+ end
+
+ context 'with an empty String as the group ID' do
+ it 'returns nil' do
+ expect(described_class.new(user, nil, group_id: '').execute).to be_nil
+ end
+ end
+
+ context 'without a project and with a group ID' do
+ it 'raises ActiveRecord::RecordNotFound if the group does not exist' do
+ finder = described_class.new(user, nil, group_id: 1)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises ActiveRecord::RecordNotFound if the user can not read the group' do
+ group = create(:group, :private)
+ finder = described_class.new(user, nil, group_id: group.id)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the group' do
+ group = create(:group, :private)
+ finder = described_class.new(nil, nil, group_id: group.id)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns the group if it exists and is readable' do
+ group = create(:group)
+ finder = described_class.new(user, nil, group_id: group.id)
+
+ expect(finder.execute).to eq(group)
+ end
+ end
+ end
+end
diff --git a/spec/finders/move_to_project_finder_spec.rb b/spec/finders/autocomplete/move_to_project_finder_spec.rb
index 1b3f44cced1..c3bc410a7f6 100644
--- a/spec/finders/move_to_project_finder_spec.rb
+++ b/spec/finders/autocomplete/move_to_project_finder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe MoveToProjectFinder do
+describe Autocomplete::MoveToProjectFinder do
let(:user) { create(:user) }
let(:project) { create(:project) }
@@ -10,14 +10,14 @@ describe MoveToProjectFinder do
let(:developer_project) { create(:project) }
let(:maintainer_project) { create(:project) }
- subject { described_class.new(user) }
-
describe '#execute' do
context 'filter' do
it 'does not return projects under Gitlab::Access::REPORTER' do
guest_project.add_guest(user)
- expect(subject.execute(project)).to be_empty
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute).to be_empty
end
it 'returns projects equal or above Gitlab::Access::REPORTER ordered by id in descending order' do
@@ -25,13 +25,17 @@ describe MoveToProjectFinder do
developer_project.add_developer(user)
maintainer_project.add_maintainer(user)
- expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project, reporter_project])
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute.to_a).to eq([maintainer_project, developer_project, reporter_project])
end
it 'does not include the source project' do
project.add_reporter(user)
- expect(subject.execute(project).to_a).to be_empty
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute.to_a).to be_empty
end
it 'does not return archived projects' do
@@ -40,7 +44,9 @@ describe MoveToProjectFinder do
other_reporter_project = create(:project)
other_reporter_project.add_reporter(user)
- expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute.to_a).to eq([other_reporter_project])
end
it 'does not return projects for which issues are disabled' do
@@ -49,39 +55,42 @@ describe MoveToProjectFinder do
other_reporter_project = create(:project)
other_reporter_project.add_reporter(user)
- expect(subject.execute(project).to_a).to eq([other_reporter_project])
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute.to_a).to eq([other_reporter_project])
end
it 'returns a page of projects ordered by id in descending order' do
- stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
+ allow(Kaminari.config).to receive(:default_per_page).and_return(2)
- reporter_project.add_reporter(user)
- developer_project.add_developer(user)
- maintainer_project.add_maintainer(user)
+ projects = create_list(:project, 2) do |project|
+ project.add_developer(user)
+ end
- expect(subject.execute(project).to_a).to eq([maintainer_project, developer_project])
+ finder = described_class.new(user, project_id: project.id)
+ page = finder.execute.to_a
+
+ expect(page.length).to eq(Kaminari.config.default_per_page)
+ expect(page[0]).to eq(projects.last)
end
it 'returns projects after the given offset id' do
- stub_const 'MoveToProjectFinder::PAGE_SIZE', 2
-
reporter_project.add_reporter(user)
developer_project.add_developer(user)
maintainer_project.add_maintainer(user)
- expect(subject.execute(project, search: nil, offset_id: maintainer_project.id).to_a).to eq([developer_project, reporter_project])
- expect(subject.execute(project, search: nil, offset_id: developer_project.id).to_a).to eq([reporter_project])
- expect(subject.execute(project, search: nil, offset_id: reporter_project.id).to_a).to be_empty
- end
- end
+ expect(described_class.new(user, project_id: project.id, offset_id: maintainer_project.id).execute.to_a)
+ .to eq([developer_project, reporter_project])
- context 'search' do
- it 'uses Project#search' do
- expect(user).to receive_message_chain(:projects_where_can_admin_issues, :search) { Project.all }
+ expect(described_class.new(user, project_id: project.id, offset_id: developer_project.id).execute.to_a)
+ .to eq([reporter_project])
- subject.execute(project, search: 'wadus')
+ expect(described_class.new(user, project_id: project.id, offset_id: reporter_project.id).execute.to_a)
+ .to be_empty
end
+ end
+ context 'search' do
it 'returns projects matching a search query' do
foo_project = create(:project)
foo_project.add_maintainer(user)
@@ -89,8 +98,11 @@ describe MoveToProjectFinder do
wadus_project = create(:project, name: 'wadus')
wadus_project.add_maintainer(user)
- expect(subject.execute(project).to_a).to eq([wadus_project, foo_project])
- expect(subject.execute(project, search: 'wadus').to_a).to eq([wadus_project])
+ expect(described_class.new(user, project_id: project.id).execute.to_a)
+ .to eq([wadus_project, foo_project])
+
+ expect(described_class.new(user, project_id: project.id, search: 'wadus').execute.to_a)
+ .to eq([wadus_project])
end
end
end
diff --git a/spec/finders/autocomplete/project_finder_spec.rb b/spec/finders/autocomplete/project_finder_spec.rb
new file mode 100644
index 00000000000..207d0598c28
--- /dev/null
+++ b/spec/finders/autocomplete/project_finder_spec.rb
@@ -0,0 +1,55 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe Autocomplete::ProjectFinder do
+ let(:user) { create(:user) }
+
+ describe '#execute' do
+ context 'without a project ID' do
+ it 'returns nil' do
+ expect(described_class.new(user).execute).to be_nil
+ end
+ end
+
+ context 'with an empty String as the project ID' do
+ it 'returns nil' do
+ expect(described_class.new(user, project_id: '').execute).to be_nil
+ end
+ end
+
+ context 'with a project ID' do
+ it 'raises ActiveRecord::RecordNotFound if the project does not exist' do
+ finder = described_class.new(user, project_id: 1)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises ActiveRecord::RecordNotFound if the user can not read the project' do
+ project = create(:project, :private)
+
+ finder = described_class.new(user, project_id: project.id)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'raises ActiveRecord::RecordNotFound if an anonymous user can not read the project' do
+ project = create(:project, :private)
+
+ finder = described_class.new(nil, project_id: project.id)
+
+ expect { finder.execute }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+
+ it 'returns the project if it exists and is readable' do
+ project = create(:project, :private)
+
+ project.add_maintainer(user)
+
+ finder = described_class.new(user, project_id: project.id)
+
+ expect(finder.execute).to eq(project)
+ end
+ end
+ end
+end
diff --git a/spec/finders/autocomplete_users_finder_spec.rb b/spec/finders/autocomplete/users_finder_spec.rb
index dcf9111776e..abd0d6b5185 100644
--- a/spec/finders/autocomplete_users_finder_spec.rb
+++ b/spec/finders/autocomplete/users_finder_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe AutocompleteUsersFinder do
+describe Autocomplete::UsersFinder do
describe '#execute' do
let!(:user1) { create(:user, username: 'johndoe') }
let!(:user2) { create(:user, :blocked, username: 'notsorandom') }
diff --git a/spec/finders/awarded_emoji_finder_spec.rb b/spec/finders/awarded_emoji_finder_spec.rb
new file mode 100644
index 00000000000..d4479df7418
--- /dev/null
+++ b/spec/finders/awarded_emoji_finder_spec.rb
@@ -0,0 +1,25 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe AwardedEmojiFinder do
+ describe '#execute' do
+ it 'returns an Array containing the awarded emoji names' do
+ user = create(:user)
+
+ create(:award_emoji, user: user, name: 'thumbsup')
+ create(:award_emoji, user: user, name: 'thumbsup')
+ create(:award_emoji, user: user, name: 'thumbsdown')
+
+ awarded = described_class.new(user).execute
+
+ expect(awarded).to eq([{ name: 'thumbsup' }, { name: 'thumbsdown' }])
+ end
+
+ it 'returns an empty Array when no user is given' do
+ awarded = described_class.new.execute
+
+ expect(awarded).to be_empty
+ end
+ end
+end
diff --git a/spec/finders/user_finder_spec.rb b/spec/finders/user_finder_spec.rb
new file mode 100644
index 00000000000..e53aa50dd33
--- /dev/null
+++ b/spec/finders/user_finder_spec.rb
@@ -0,0 +1,43 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe UserFinder do
+ describe '#execute' do
+ context 'when the user exists' do
+ it 'returns the user' do
+ user = create(:user)
+ found = described_class.new(id: user.id).execute
+
+ expect(found).to eq(user)
+ end
+ end
+
+ context 'when the user does not exist' do
+ it 'returns nil' do
+ found = described_class.new(id: 1).execute
+
+ expect(found).to be_nil
+ end
+ end
+ end
+
+ describe '#execute!' do
+ context 'when the user exists' do
+ it 'returns the user' do
+ user = create(:user)
+ found = described_class.new(id: user.id).execute!
+
+ expect(found).to eq(user)
+ end
+ end
+
+ context 'when the user does not exist' do
+ it 'raises ActiveRecord::RecordNotFound' do
+ finder = described_class.new(id: 1)
+
+ expect { finder.execute! }.to raise_error(ActiveRecord::RecordNotFound)
+ end
+ end
+ end
+end
diff --git a/spec/javascripts/ide/stores/actions/file_spec.js b/spec/javascripts/ide/stores/actions/file_spec.js
index 72eb20bdc87..bca2033ff97 100644
--- a/spec/javascripts/ide/stores/actions/file_spec.js
+++ b/spec/javascripts/ide/stores/actions/file_spec.js
@@ -352,10 +352,22 @@ describe('IDE store file actions', () => {
it('calls also getBaseRawFileData service method', done => {
spyOn(service, 'getBaseRawFileData').and.returnValue(Promise.resolve('baseraw'));
+ store.state.currentProjectId = 'gitlab-org/gitlab-ce';
+ store.state.currentMergeRequestId = '1';
+ store.state.projects = {
+ 'gitlab-org/gitlab-ce': {
+ mergeRequests: {
+ 1: {
+ baseCommitSha: 'SHA',
+ },
+ },
+ },
+ };
+
tmpFile.mrChange = { new_file: false };
store
- .dispatch('getRawFileData', { path: tmpFile.path, baseSha: 'SHA' })
+ .dispatch('getRawFileData', { path: tmpFile.path })
.then(() => {
expect(service.getBaseRawFileData).toHaveBeenCalledWith(tmpFile, 'SHA');
expect(tmpFile.baseRaw).toBe('baseraw');
@@ -392,10 +404,7 @@ describe('IDE store file actions', () => {
const dispatch = jasmine.createSpy('dispatch');
actions
- .getRawFileData(
- { state: store.state, commit() {}, dispatch },
- { path: tmpFile.path, baseSha: tmpFile.baseSha },
- )
+ .getRawFileData({ state: store.state, commit() {}, dispatch }, { path: tmpFile.path })
.then(done.fail)
.catch(() => {
expect(dispatch).toHaveBeenCalledWith('setErrorMessage', {
@@ -404,7 +413,6 @@ describe('IDE store file actions', () => {
actionText: 'Please try again',
actionPayload: {
path: tmpFile.path,
- baseSha: tmpFile.baseSha,
},
});
diff --git a/spec/javascripts/jobs/commit_block_spec.js b/spec/javascripts/jobs/commit_block_spec.js
new file mode 100644
index 00000000000..f755a5042b5
--- /dev/null
+++ b/spec/javascripts/jobs/commit_block_spec.js
@@ -0,0 +1,73 @@
+import Vue from 'vue';
+import component from '~/jobs/components/commit_block.vue';
+import mountComponent from '../helpers/vue_mount_component_helper';
+
+describe('Commit block', () => {
+ const Component = Vue.extend(component);
+ let vm;
+
+ const props = {
+ pipelineShortSha: '1f0fb84f',
+ pipelineShaPath: 'commit/1f0fb84fb6770d74d97eee58118fd3909cd4f48c',
+ mergeRequestReference: '!21244',
+ mergeRequestPath: 'merge_requests/21244',
+ gitCommitTitlte: 'Regenerate pot files',
+ };
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ describe('pipeline short sha', () => {
+ beforeEach(() => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+ });
+
+ it('renders pipeline short sha link', () => {
+ expect(vm.$el.querySelector('.js-commit-sha').getAttribute('href')).toEqual(props.pipelineShaPath);
+ expect(vm.$el.querySelector('.js-commit-sha').textContent.trim()).toEqual(props.pipelineShortSha);
+ });
+
+ it('renders clipboard button', () => {
+ expect(vm.$el.querySelector('button').getAttribute('data-clipboard-text')).toEqual(props.pipelineShortSha);
+ });
+ });
+
+ describe('with merge request', () => {
+ it('renders merge request link and reference', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit').getAttribute('href')).toEqual(props.mergeRequestPath);
+ expect(vm.$el.querySelector('.js-link-commit').textContent.trim()).toEqual(props.mergeRequestReference);
+
+ });
+ });
+
+ describe('without merge request', () => {
+ it('does not render merge request', () => {
+ const copyProps = Object.assign({}, props);
+ delete copyProps.mergeRequestPath;
+ delete copyProps.mergeRequestReference;
+
+ vm = mountComponent(Component, {
+ ...copyProps,
+ });
+
+ expect(vm.$el.querySelector('.js-link-commit')).toBeNull();
+ });
+ });
+
+ describe('git commit title', () => {
+ it('renders git commit title', () => {
+ vm = mountComponent(Component, {
+ ...props,
+ });
+
+ expect(vm.$el.textContent).toContain(props.gitCommitTitlte);
+ });
+ });
+});
diff --git a/spec/javascripts/jobs/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/banzai/filter/spaced_link_filter_spec.rb b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
new file mode 100644
index 00000000000..4463c011522
--- /dev/null
+++ b/spec/lib/banzai/filter/spaced_link_filter_spec.rb
@@ -0,0 +1,66 @@
+require 'spec_helper'
+
+describe Banzai::Filter::SpacedLinkFilter do
+ include FilterSpecHelper
+
+ let(:link) { '[example](page slug)' }
+
+ it 'converts slug with spaces to a link' do
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq 'example'
+ expect(doc.at_css('a')['href']).to eq 'page%20slug'
+ expect(doc.at_css('p')).to eq nil
+ end
+
+ it 'converts slug with spaces and a title to a link' do
+ link = '[example](page slug "title")'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a').text).to eq 'example'
+ expect(doc.at_css('a')['href']).to eq 'page%20slug'
+ expect(doc.at_css('a')['title']).to eq 'title'
+ expect(doc.at_css('p')).to eq nil
+ end
+
+ it 'does nothing when markdown_engine is redcarpet' do
+ exp = act = link
+ expect(filter(act, markdown_engine: :redcarpet).to_html).to eq exp
+ end
+
+ it 'does nothing with empty text' do
+ link = '[](page slug)'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a')).to eq nil
+ end
+
+ it 'does nothing with an empty slug' do
+ link = '[example]()'
+ doc = filter("See #{link}")
+
+ expect(doc.at_css('a')).to eq nil
+ end
+
+ it 'converts multiple URLs' do
+ link1 = '[first](slug one)'
+ link2 = '[second](http://example.com/slug two)'
+ doc = filter("See #{link1} and #{link2}")
+
+ found_links = doc.css('a')
+
+ expect(found_links.size).to eq(2)
+ expect(found_links[0].text).to eq 'first'
+ expect(found_links[0]['href']).to eq 'slug%20one'
+ expect(found_links[1].text).to eq 'second'
+ expect(found_links[1]['href']).to eq 'http://example.com/slug%20two'
+ end
+
+ described_class::IGNORE_PARENTS.each do |elem|
+ it "ignores valid links contained inside '#{elem}' element" do
+ exp = act = "<#{elem}>See #{link}</#{elem}>"
+
+ expect(filter(act).to_html).to eq exp
+ end
+ end
+end
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/data_builder/build_spec.rb b/spec/lib/gitlab/data_builder/build_spec.rb
index ee91decafad..14fe196a986 100644
--- a/spec/lib/gitlab/data_builder/build_spec.rb
+++ b/spec/lib/gitlab/data_builder/build_spec.rb
@@ -15,6 +15,7 @@ describe Gitlab::DataBuilder::Build do
it { expect(data[:build_id]).to eq(build.id) }
it { expect(data[:build_status]).to eq(build.status) }
it { expect(data[:build_allow_failure]).to eq(false) }
+ it { expect(data[:build_failure_reason]).to eq(build.failure_reason) }
it { expect(data[:project_id]).to eq(build.project.id) }
it { expect(data[:project_name]).to eq(build.project.full_name) }
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 35a6fc94753..17348b01006 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -542,7 +542,7 @@ describe Gitlab::Git::Repository, :seed_helper do
Gitlab::Shell.new.remove_repository('default', 'my_project')
end
- shared_examples 'repository mirror fecthing' do
+ shared_examples 'repository mirror fetching' do
it 'fetches a repository as a mirror remote' do
subject
@@ -569,11 +569,11 @@ describe Gitlab::Git::Repository, :seed_helper do
end
context 'with gitaly enabled' do
- it_behaves_like 'repository mirror fecthing'
+ it_behaves_like 'repository mirror fetching'
end
context 'with gitaly enabled', :skip_gitaly_mock do
- it_behaves_like 'repository mirror fecthing'
+ it_behaves_like 'repository mirror fetching'
end
def new_repository_path
diff --git a/spec/lib/gitlab/gitaly_client/diff_spec.rb b/spec/lib/gitlab/gitaly_client/diff_spec.rb
index 00a31ac0b96..ec7ab2fdedb 100644
--- a/spec/lib/gitlab/gitaly_client/diff_spec.rb
+++ b/spec/lib/gitlab/gitaly_client/diff_spec.rb
@@ -9,7 +9,9 @@ describe Gitlab::GitalyClient::Diff do
new_mode: 0100644,
from_id: '357406f3075a57708d0163752905cc1576fceacc',
to_id: '8e5177d718c561d36efde08bad36b43687ee6bf0',
- patch: 'a' * 100
+ patch: 'a' * 100,
+ collapsed: false,
+ too_large: false
}
end
@@ -22,6 +24,8 @@ describe Gitlab::GitalyClient::Diff do
it { is_expected.to respond_to(:from_id) }
it { is_expected.to respond_to(:to_id) }
it { is_expected.to respond_to(:patch) }
+ it { is_expected.to respond_to(:collapsed) }
+ it { is_expected.to respond_to(:too_large) }
describe '#==' do
it { expect(subject).to eq(described_class.new(diff_fields)) }
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/import_export/all_models.yml b/spec/lib/gitlab/import_export/all_models.yml
index e9a1932407d..b4269bd5786 100644
--- a/spec/lib/gitlab/import_export/all_models.yml
+++ b/spec/lib/gitlab/import_export/all_models.yml
@@ -210,7 +210,6 @@ project:
- flowdock_service
- assembla_service
- asana_service
-- gemnasium_service
- slack_service
- microsoft_teams_service
- mattermost_service
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/award_emoji_spec.rb b/spec/models/award_emoji_spec.rb
index b909e04dfc3..3f52091698c 100644
--- a/spec/models/award_emoji_spec.rb
+++ b/spec/models/award_emoji_spec.rb
@@ -77,4 +77,27 @@ describe AwardEmoji do
end
end
end
+
+ describe '.award_counts_for_user' do
+ let(:user) { create(:user) }
+
+ before do
+ create(:award_emoji, user: user, name: 'thumbsup')
+ create(:award_emoji, user: user, name: 'thumbsup')
+ create(:award_emoji, user: user, name: 'thumbsdown')
+ create(:award_emoji, user: user, name: '+1')
+ end
+
+ it 'returns the awarded emoji in descending order' do
+ awards = described_class.award_counts_for_user(user)
+
+ expect(awards).to eq('thumbsup' => 2, 'thumbsdown' => 1, '+1' => 1)
+ end
+
+ it 'limits the returned number of rows' do
+ awards = described_class.award_counts_for_user(user, 1)
+
+ expect(awards).to eq('thumbsup' => 2)
+ end
+ end
end
diff --git a/spec/models/clusters/applications/prometheus_spec.rb b/spec/models/clusters/applications/prometheus_spec.rb
index 7454be3ab2f..26b75c75e1d 100644
--- a/spec/models/clusters/applications/prometheus_spec.rb
+++ b/spec/models/clusters/applications/prometheus_spec.rb
@@ -154,20 +154,17 @@ describe Clusters::Applications::Prometheus do
end
describe '#install_command' do
- let(:kubeclient) { double('kubernetes client') }
let(:prometheus) { create(:clusters_applications_prometheus) }
- it 'returns an instance of Gitlab::Kubernetes::Helm::InstallCommand' do
- expect(prometheus.install_command).to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand)
- end
+ subject { prometheus.install_command }
- it 'should be initialized with 3 arguments' do
- command = prometheus.install_command
+ it { is_expected.to be_an_instance_of(Gitlab::Kubernetes::Helm::InstallCommand) }
- expect(command.name).to eq('prometheus')
- expect(command.chart).to eq('stable/prometheus')
- expect(command.version).to eq('6.7.3')
- expect(command.files).to eq(prometheus.files)
+ it 'should be initialized with 3 arguments' do
+ expect(subject.name).to eq('prometheus')
+ expect(subject.chart).to eq('stable/prometheus')
+ expect(subject.version).to eq('6.7.3')
+ expect(subject.files).to eq(prometheus.files)
end
context 'application failed to install previously' do
diff --git a/spec/models/concerns/optionally_search_spec.rb b/spec/models/concerns/optionally_search_spec.rb
new file mode 100644
index 00000000000..ff4212ddf18
--- /dev/null
+++ b/spec/models/concerns/optionally_search_spec.rb
@@ -0,0 +1,44 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe OptionallySearch do
+ let(:model) do
+ Class.new(ActiveRecord::Base) do
+ self.table_name = 'users'
+
+ include OptionallySearch
+ end
+ end
+
+ describe '.search' do
+ it 'raises NotImplementedError' do
+ expect { model.search('foo') }.to raise_error(NotImplementedError)
+ end
+ end
+
+ describe '.optionally_search' do
+ context 'when a query is given' do
+ it 'delegates to the search method' do
+ expect(model)
+ .to receive(:search)
+ .with('foo')
+
+ model.optionally_search('foo')
+ end
+ end
+
+ context 'when no query is given' do
+ it 'returns the current relation' do
+ expect(model.optionally_search).to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+
+ context 'when an empty query is given' do
+ it 'returns the current relation' do
+ expect(model.optionally_search(''))
+ .to be_a_kind_of(ActiveRecord::Relation)
+ end
+ end
+ end
+end
diff --git a/spec/models/fork_network_member_spec.rb b/spec/models/fork_network_member_spec.rb
index 25bf596fddc..60d04562e6c 100644
--- a/spec/models/fork_network_member_spec.rb
+++ b/spec/models/fork_network_member_spec.rb
@@ -11,7 +11,7 @@ describe ForkNetworkMember do
let(:fork_network) { fork_network_member.fork_network }
it 'removes the fork network if it was the last member' do
- fork_network.fork_network_members.destroy_all
+ fork_network.fork_network_members.destroy_all # rubocop: disable DestroyAll
expect(ForkNetwork.count).to eq(0)
end
diff --git a/spec/models/hooks/system_hook_spec.rb b/spec/models/hooks/system_hook_spec.rb
index 01129df1107..edd1cb455af 100644
--- a/spec/models/hooks/system_hook_spec.rb
+++ b/spec/models/hooks/system_hook_spec.rb
@@ -73,7 +73,7 @@ describe SystemHook do
it "project_destroy hook" do
project.add_maintainer(user)
- project.project_members.destroy_all
+ project.project_members.destroy_all # rubocop: disable DestroyAll
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_team/,
@@ -110,7 +110,7 @@ describe SystemHook do
it 'group member destroy hook' do
group.add_maintainer(user)
- group.group_members.destroy_all
+ group.group_members.destroy_all # rubocop: disable DestroyAll
expect(WebMock).to have_requested(:post, system_hook.url).with(
body: /user_remove_from_group/,
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 6258bfa232f..48f4e53b93e 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -1764,7 +1764,7 @@ describe MergeRequest do
context 'with no discussions' do
before do
- merge_request.notes.destroy_all
+ merge_request.notes.destroy_all # rubocop: disable DestroyAll
end
it 'returns true' do
diff --git a/spec/models/project_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_services/gemnasium_service_spec.rb b/spec/models/project_services/gemnasium_service_spec.rb
deleted file mode 100644
index 6e417323e40..00000000000
--- a/spec/models/project_services/gemnasium_service_spec.rb
+++ /dev/null
@@ -1,74 +0,0 @@
-require 'spec_helper'
-
-describe GemnasiumService do
- describe "Associations" do
- it { is_expected.to belong_to :project }
- it { is_expected.to have_one :service_hook }
- end
-
- describe 'Validations' do
- context 'when service is active' do
- before do
- subject.active = true
- end
-
- it { is_expected.to validate_presence_of(:token) }
- it { is_expected.to validate_presence_of(:api_key) }
- end
-
- context 'when service is inactive' do
- before do
- subject.active = false
- end
-
- it { is_expected.not_to validate_presence_of(:token) }
- it { is_expected.not_to validate_presence_of(:api_key) }
- end
- end
-
- describe "deprecated?" do
- let(:project) { create(:project, :repository) }
- let(:gemnasium_service) { described_class.new }
-
- before do
- allow(gemnasium_service).to receive_messages(
- project_id: project.id,
- project: project,
- service_hook: true,
- token: 'verySecret',
- api_key: 'GemnasiumUserApiKey'
- )
- end
-
- it "is true" do
- expect(gemnasium_service.deprecated?).to be true
- end
-
- it "can't create a new service" do
- expect(gemnasium_service.save).to be false
- expect(gemnasium_service.errors[:base].first)
- .to eq('Gemnasium has been acquired by GitLab in January 2018. Since May 15, 2018, the service provided by Gemnasium is no longer available.')
- end
- end
-
- describe "Execute" do
- let(:user) { create(:user) }
- let(:project) { create(:project, :repository) }
- let(:gemnasium_service) { described_class.new }
- let(:sample_data) { Gitlab::DataBuilder::Push.build_sample(project, user) }
-
- before do
- allow(gemnasium_service).to receive_messages(
- project_id: project.id,
- project: project,
- service_hook: true,
- token: 'verySecret',
- api_key: 'GemnasiumUserApiKey'
- )
- end
- it "calls Gemnasium service" do
- expect(Gemnasium::GitlabService).to receive(:execute).with(an_instance_of(Hash)).once
- gemnasium_service.execute(sample_data)
- end
- end
-end
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index ac9ff59b9b5..54f1a0e38a5 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -27,13 +27,13 @@ describe JiraService do
end
end
- describe "Associations" do
+ describe 'Associations' do
it { is_expected.to belong_to :project }
it { is_expected.to have_one :service_hook }
it { is_expected.to allow_value(nil).for(:jira_issue_transition_id) }
- it { is_expected.to allow_value("1,2,3").for(:jira_issue_transition_id) }
- it { is_expected.to allow_value("1;2;3").for(:jira_issue_transition_id) }
- it { is_expected.not_to allow_value("a,b,cd").for(:jira_issue_transition_id) }
+ it { is_expected.to allow_value('1,2,3').for(:jira_issue_transition_id) }
+ it { is_expected.to allow_value('1;2;3').for(:jira_issue_transition_id) }
+ it { is_expected.not_to allow_value('a,b,cd').for(:jira_issue_transition_id) }
end
describe 'Validations' do
@@ -116,143 +116,142 @@ describe JiraService do
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
- let(:project) { create(:project) }
- let(:merge_request) { create(:merge_request) }
-
- before do
- @jira_service = described_class.new
- allow(@jira_service).to receive_messages(
- project_id: project.id,
- project: project,
- service_hook: true,
- url: 'http://jira.example.com',
- username: 'gitlab_jira_username',
- password: 'gitlab_jira_password',
- jira_issue_transition_id: "999"
- )
+ let(:project) { create(:project, :repository) }
- # These stubs are needed to test JiraService#close_issue.
- # We close the issue then do another request to API to check if it got closed.
- # Here is stubbed the API return with a closed and an opened issues.
- open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { "id" => "JIRA-123" })
- closed_issue = open_issue.dup
- allow(open_issue).to receive(:resolution).and_return(false)
- allow(closed_issue).to receive(:resolution).and_return(true)
- allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
-
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return("JIRA-123")
- allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
-
- @jira_service.save
-
- project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
- @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
- @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment'
- @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
-
- WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
- WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
- WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
- WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
- end
+ shared_examples 'close_issue' do
+ before do
+ @jira_service = described_class.new
+ allow(@jira_service).to receive_messages(
+ project_id: project.id,
+ project: project,
+ service_hook: true,
+ url: 'http://jira.example.com',
+ username: 'gitlab_jira_username',
+ password: 'gitlab_jira_password',
+ jira_issue_transition_id: '999'
+ )
- it "calls JIRA API" do
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ # These stubs are needed to test JiraService#close_issue.
+ # We close the issue then do another request to API to check if it got closed.
+ # Here is stubbed the API return with a closed and an opened issues.
+ open_issue = JIRA::Resource::Issue.new(@jira_service.client, attrs: { 'id' => 'JIRA-123' })
+ closed_issue = open_issue.dup
+ allow(open_issue).to receive(:resolution).and_return(false)
+ allow(closed_issue).to receive(:resolution).and_return(true)
+ allow(JIRA::Resource::Issue).to receive(:find).and_return(open_issue, closed_issue)
- expect(WebMock).to have_requested(:post, @comment_url).with(
- body: /Issue solved with/
- ).once
- end
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:key).and_return('JIRA-123')
+ allow(JIRA::Resource::Remotelink).to receive(:all).and_return([])
- # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
- # for more information
- it "creates Remote Link reference in JIRA for comment" do
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
-
- favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
-
- # Creates comment
- expect(WebMock).to have_requested(:post, @comment_url)
- # Creates Remote Link in JIRA issue fields
- expect(WebMock).to have_requested(:post, @remote_link_url).with(
- body: hash_including(
- GlobalID: "GitLab",
- object: {
- url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}",
- title: "GitLab: Solved by commit #{merge_request.diff_head_sha}.",
- icon: { title: "GitLab", url16x16: favicon_path },
- status: { resolved: true }
- }
- )
- ).once
- end
+ @jira_service.save
- it "does not send comment or remote links to issues already closed" do
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
+ project_issues_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123'
+ @transitions_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/transitions'
+ @comment_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/comment'
+ @remote_link_url = 'http://jira.example.com/rest/api/2/issue/JIRA-123/remotelink'
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ WebMock.stub_request(:get, project_issues_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @comment_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ WebMock.stub_request(:post, @remote_link_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password))
+ end
- expect(WebMock).not_to have_requested(:post, @comment_url)
- expect(WebMock).not_to have_requested(:post, @remote_link_url)
- end
+ it 'calls JIRA API' do
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
- it "does not send comment or remote links to issues with unknown resolution" do
- allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: /Issue solved with/
+ ).once
+ end
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ # Check https://developer.atlassian.com/jiradev/jira-platform/guides/other/guide-jira-remote-issue-links/fields-in-remote-issue-links
+ # for more information
+ it 'creates Remote Link reference in JIRA for comment' do
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+
+ favicon_path = "http://localhost/assets/#{find_asset('favicon.png').digest_path}"
+
+ # Creates comment
+ expect(WebMock).to have_requested(:post, @comment_url)
+ # Creates Remote Link in JIRA issue fields
+ expect(WebMock).to have_requested(:post, @remote_link_url).with(
+ body: hash_including(
+ GlobalID: 'GitLab',
+ object: {
+ url: "#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}",
+ title: "GitLab: Solved by commit #{commit_id}.",
+ icon: { title: 'GitLab', url16x16: favicon_path },
+ status: { resolved: true }
+ }
+ )
+ ).once
+ end
- expect(WebMock).not_to have_requested(:post, @comment_url)
- expect(WebMock).not_to have_requested(:post, @remote_link_url)
- end
+ it 'does not send comment or remote links to issues already closed' do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:resolution).and_return(true)
- it "references the GitLab commit/merge request" do
- stub_config_setting(base_url: custom_base_url)
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
+ end
- expect(WebMock).to have_requested(:post, @comment_url).with(
- body: %r{#{custom_base_url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
- ).once
- end
+ it 'does not send comment or remote links to issues with unknown resolution' do
+ allow_any_instance_of(JIRA::Resource::Issue).to receive(:respond_to?).with(:resolution).and_return(false)
- it "references the GitLab commit/merge request (relative URL)" do
- stub_config_setting(relative_url_root: '/gitlab')
- stub_config_setting(url: Settings.send(:build_gitlab_url))
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
- allow(described_class).to receive(:default_url_options) do
- { script_name: '/gitlab' }
+ expect(WebMock).not_to have_requested(:post, @comment_url)
+ expect(WebMock).not_to have_requested(:post, @remote_link_url)
end
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ it 'references the GitLab commit' do
+ stub_config_setting(base_url: custom_base_url)
- expect(WebMock).to have_requested(:post, @comment_url).with(
- body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{merge_request.diff_head_sha}}
- ).once
- end
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
+
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: %r{#{custom_base_url}/#{project.full_path}/commit/#{commit_id}}
+ ).once
+ end
+
+ it 'references the GitLab commit' do
+ stub_config_setting(relative_url_root: '/gitlab')
+ stub_config_setting(url: Settings.send(:build_gitlab_url))
+
+ allow(described_class).to receive(:default_url_options) do
+ { script_name: '/gitlab' }
+ end
+
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
- context '#close_issue' do
- it "logs exception when transition id is not valid" do
+ expect(WebMock).to have_requested(:post, @comment_url).with(
+ body: %r{#{Gitlab.config.gitlab.url}/#{project.full_path}/commit/#{commit_id}}
+ ).once
+ end
+
+ it 'logs exception when transition id is not valid' do
allow(Rails.logger).to receive(:info)
- WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise("Bad Request")
+ WebMock.stub_request(:post, @transitions_url).with(basic_auth: %w(gitlab_jira_username gitlab_jira_password)).and_raise('Bad Request')
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
- expect(Rails.logger).to have_received(:info).with("JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request")
+ expect(Rails.logger).to have_received(:info).with('JiraService Issue Transition failed message ERROR: http://jira.example.com - Bad Request')
end
- it "calls the api with jira_issue_transition_id" do
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ it 'calls the api with jira_issue_transition_id' do
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
expect(WebMock).to have_requested(:post, @transitions_url).with(
body: /999/
).once
end
- context "when have multiple transition ids" do
- it "calls the api with transition ids separated by comma" do
- allow(@jira_service).to receive_messages(jira_issue_transition_id: "1,2,3")
+ context 'when have multiple transition ids' do
+ it 'calls the api with transition ids separated by comma' do
+ allow(@jira_service).to receive_messages(jira_issue_transition_id: '1,2,3')
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with(
@@ -261,10 +260,10 @@ describe JiraService do
end
end
- it "calls the api with transition ids separated by semicolon" do
- allow(@jira_service).to receive_messages(jira_issue_transition_id: "1;2;3")
+ it 'calls the api with transition ids separated by semicolon' do
+ allow(@jira_service).to receive_messages(jira_issue_transition_id: '1;2;3')
- @jira_service.close_issue(merge_request, ExternalIssue.new("JIRA-123", project))
+ @jira_service.close_issue(resource, ExternalIssue.new('JIRA-123', project))
1.upto(3) do |transition_id|
expect(WebMock).to have_requested(:post, @transitions_url).with(
@@ -274,6 +273,20 @@ describe JiraService do
end
end
end
+
+ context 'when resource is a merge request' do
+ let(:resource) { create(:merge_request) }
+ let(:commit_id) { resource.diff_head_sha }
+
+ it_behaves_like 'close_issue'
+ end
+
+ context 'when resource is a commit' do
+ let(:resource) { project.commit('master') }
+ let(:commit_id) { resource.id }
+
+ it_behaves_like 'close_issue'
+ end
end
describe '#test_settings' do
@@ -321,17 +334,17 @@ describe JiraService do
end
end
- describe "Stored password invalidation" do
+ describe 'Stored password invalidation' do
let(:project) { create(:project) }
- context "when a password was previously set" do
+ context 'when a password was previously set' do
before do
@jira_service = described_class.create!(
project: project,
properties: {
url: 'http://jira.example.com/web',
username: 'mic',
- password: "password"
+ password: 'password'
}
)
end
@@ -370,10 +383,10 @@ describe JiraService do
@jira_service.url = 'http://jira_edited.example.com/rweb'
@jira_service.save
- expect(@jira_service.password).to eq("password")
+ expect(@jira_service.password).to eq('password')
end
- it 'reset password if api url set to ""' do
+ it 'reset password if api url set to empty' do
@jira_service.api_url = ''
@jira_service.save
@@ -440,7 +453,7 @@ describe JiraService do
it 'is initialized' do
expect(@service.title).to eq('JIRA')
- expect(@service.description).to eq("Jira issue tracker")
+ expect(@service.description).to eq('Jira issue tracker')
end
end
@@ -454,7 +467,7 @@ describe JiraService do
@service.destroy!
end
- it "is correct" do
+ it 'is correct' do
expect(@service.title).to eq('Jira One')
expect(@service.description).to eq('Jira One issue tracker')
end
@@ -476,7 +489,7 @@ describe JiraService do
it 'is initialized' do
expect(@service.options[:use_cookies]).to eq(true)
- expect(@service.options[:additional_cookies]).to eq(["OBBasicAuth=fromDialog"])
+ expect(@service.options[:additional_cookies]).to eq(['OBBasicAuth=fromDialog'])
end
end
end
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index d8a5e5f6869..8cb706b54b0 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -42,7 +42,6 @@ describe Project do
it { is_expected.to have_one(:assembla_service) }
it { is_expected.to have_one(:slack_slash_commands_service) }
it { is_expected.to have_one(:mattermost_slash_commands_service) }
- it { is_expected.to have_one(:gemnasium_service) }
it { is_expected.to have_one(:buildkite_service) }
it { is_expected.to have_one(:bamboo_service) }
it { is_expected.to have_one(:teamcity_service) }
@@ -1478,6 +1477,53 @@ describe Project do
end
end
+ describe '.optionally_search' do
+ let(:project) { create(:project) }
+
+ it 'searches for projects matching the query if one is given' do
+ relation = described_class.optionally_search(project.name)
+
+ expect(relation).to eq([project])
+ end
+
+ it 'returns the current relation if no search query is given' do
+ relation = described_class.where(id: project.id)
+
+ expect(relation.optionally_search).to eq(relation)
+ end
+ end
+
+ describe '.paginate_in_descending_order_using_id' do
+ let!(:project1) { create(:project) }
+ let!(:project2) { create(:project) }
+
+ it 'orders the relation in descending order' do
+ expect(described_class.paginate_in_descending_order_using_id)
+ .to eq([project2, project1])
+ end
+
+ it 'applies a limit to the relation' do
+ expect(described_class.paginate_in_descending_order_using_id(limit: 1))
+ .to eq([project2])
+ end
+
+ it 'limits projects by and ID when given' do
+ expect(described_class.paginate_in_descending_order_using_id(before: project2.id))
+ .to eq([project1])
+ end
+ end
+
+ describe '.including_namespace_and_owner' do
+ it 'eager loads the namespace and namespace owner' do
+ create(:project)
+
+ row = described_class.eager_load_namespace_and_owner.to_a.first
+ recorder = ActiveRecord::QueryRecorder.new { row.namespace.owner }
+
+ expect(recorder.count).to be_zero
+ end
+ end
+
describe '#expire_caches_before_rename' do
let(:project) { create(:project, :repository) }
let(:repo) { double(:repo, exists?: true) }
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index f5e2c977104..9763477a711 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -346,17 +346,55 @@ describe User do
end
end
- describe '.todo_authors' do
- it 'filters users' do
- create :user
- user_2 = create :user
- user_3 = create :user
- current_user = create :user
- create(:todo, user: current_user, author: user_2, state: :done)
- create(:todo, user: current_user, author: user_3, state: :pending)
+ describe '.limit_to_todo_authors' do
+ context 'when filtering by todo authors' do
+ let(:user1) { create(:user) }
+ let(:user2) { create(:user) }
- expect(described_class.todo_authors(current_user.id, 'pending')).to eq [user_3]
- expect(described_class.todo_authors(current_user.id, 'done')).to eq [user_2]
+ before do
+ create(:todo, user: user1, author: user1, state: :done)
+ create(:todo, user: user2, author: user2, state: :pending)
+ end
+
+ it 'only returns users that have authored todos' do
+ users = described_class.limit_to_todo_authors(
+ user: user2,
+ with_todos: true,
+ todo_state: :pending
+ )
+
+ expect(users).to eq([user2])
+ end
+
+ it 'ignores users that do not have a todo in the matching state' do
+ users = described_class.limit_to_todo_authors(
+ user: user1,
+ with_todos: true,
+ todo_state: :pending
+ )
+
+ expect(users).to be_empty
+ end
+ end
+
+ context 'when not filtering by todo authors' do
+ it 'returns the input relation' do
+ user1 = create(:user)
+ user2 = create(:user)
+ rel = described_class.limit_to_todo_authors(user: user1)
+
+ expect(rel).to include(user1, user2)
+ end
+ end
+
+ context 'when no user is provided' do
+ it 'returns the input relation' do
+ user1 = create(:user)
+ user2 = create(:user)
+ rel = described_class.limit_to_todo_authors
+
+ expect(rel).to include(user1, user2)
+ end
end
end
end
@@ -2901,4 +2939,86 @@ describe User do
let(:uploader_class) { AttachmentUploader }
end
end
+
+ describe '.union_with_user' do
+ context 'when no user ID is provided' do
+ it 'returns the input relation' do
+ user = create(:user)
+
+ expect(described_class.union_with_user).to eq([user])
+ end
+ end
+
+ context 'when a user ID is provided' do
+ it 'includes the user object in the returned relation' do
+ user1 = create(:user)
+ user2 = create(:user)
+ users = described_class.where(id: user1.id).union_with_user(user2.id)
+
+ expect(users).to include(user1)
+ expect(users).to include(user2)
+ end
+
+ it 'does not re-apply any WHERE conditions on the outer query' do
+ relation = described_class.where(id: 1).union_with_user(2)
+
+ expect(relation.arel.where_sql).to be_nil
+ end
+ end
+ end
+
+ describe '.optionally_search' do
+ context 'using nil as the argument' do
+ it 'returns the current relation' do
+ user = create(:user)
+
+ expect(described_class.optionally_search).to eq([user])
+ end
+ end
+
+ context 'using an empty String as the argument' do
+ it 'returns the current relation' do
+ user = create(:user)
+
+ expect(described_class.optionally_search('')).to eq([user])
+ end
+ end
+
+ context 'using a non-empty String' do
+ it 'returns users matching the search query' do
+ user1 = create(:user)
+ create(:user)
+
+ expect(described_class.optionally_search(user1.name)).to eq([user1])
+ end
+ end
+ end
+
+ describe '.where_not_in' do
+ context 'without an argument' do
+ it 'returns the current relation' do
+ user = create(:user)
+
+ expect(described_class.where_not_in).to eq([user])
+ end
+ end
+
+ context 'using a list of user IDs' do
+ it 'excludes the users from the returned relation' do
+ user1 = create(:user)
+ user2 = create(:user)
+
+ expect(described_class.where_not_in([user2.id])).to eq([user1])
+ end
+ end
+ end
+
+ describe '.reorder_by_name' do
+ it 'reorders the input relation' do
+ user1 = create(:user, name: 'A')
+ user2 = create(:user, name: 'B')
+
+ expect(described_class.reorder_by_name).to eq([user1, user2])
+ end
+ end
end
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/events_spec.rb b/spec/requests/api/events_spec.rb
index e6a61fdcf39..573eebe314d 100644
--- a/spec/requests/api/events_spec.rb
+++ b/spec/requests/api/events_spec.rb
@@ -2,9 +2,9 @@ require 'spec_helper'
describe API::Events do
include ApiHelpers
+
let(:user) { create(:user) }
let(:non_member) { create(:user) }
- let(:other_user) { create(:user, username: 'otheruser') }
let(:private_project) { create(:project, :private, creator_id: user.id, namespace: user.namespace) }
let(:closed_issue) { create(:closed_issue, project: private_project, author: user) }
let!(:closed_issue_event) { create(:event, project: private_project, author: user, target: closed_issue, action: Event::CLOSED, created_at: Date.new(2016, 12, 30)) }
@@ -28,12 +28,52 @@ describe API::Events do
expect(json_response.size).to eq(1)
end
end
+
+ context 'when the requesting token has "read_user" scope' do
+ let(:token) { create(:personal_access_token, scopes: ['read_user'], user: user) }
+
+ it 'returns users events' do
+ get api('/events?action=closed&target_type=issue&after=2016-12-1&before=2016-12-31', personal_access_token: token)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(response).to include_pagination_headers
+ expect(json_response).to be_an Array
+ expect(json_response.size).to eq(1)
+ end
+ end
+
+ context 'when the requesting token does not have "read_user" or "api" scope' do
+ let(:token_without_scopes) { create(:personal_access_token, scopes: ['read_repository'], user: user) }
+
+ it 'returns a "403" response' do
+ get api('/events', personal_access_token: token_without_scopes)
+
+ expect(response).to have_gitlab_http_status(403)
+ end
+ end
end
describe 'GET /users/:id/events' do
- context "as a user that cannot see the event's project" do
- it 'returns no events' do
- get api("/users/#{user.id}/events", other_user)
+ context "as a user that cannot see another user" do
+ it 'returns a "404" response' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(non_member, :read_user, user).and_return(false)
+
+ get api("/users/#{user.id}/events", non_member)
+
+ expect(response).to have_gitlab_http_status(200)
+ expect(json_response).to be_empty
+ end
+ end
+
+ context "as a user token that cannot see another user" do
+ let(:non_member_token) { create(:personal_access_token, scopes: ['read_user'], user: non_member) }
+
+ it 'returns a "404" response' do
+ allow(Ability).to receive(:allowed?).and_call_original
+ allow(Ability).to receive(:allowed?).with(non_member, :read_user, user).and_return(false)
+
+ get api("/users/#{user.id}/events", personal_access_token: non_member_token)
expect(response).to have_gitlab_http_status(200)
expect(json_response).to be_empty
diff --git a/spec/requests/api/project_hooks_spec.rb b/spec/requests/api/project_hooks_spec.rb
index bc45a63d9f1..d3f81cc038d 100644
--- a/spec/requests/api/project_hooks_spec.rb
+++ b/spec/requests/api/project_hooks_spec.rb
@@ -83,11 +83,6 @@ describe API::ProjectHooks, 'ProjectHooks' do
expect(response).to have_gitlab_http_status(403)
end
end
-
- it "returns a 404 error if hook id is not available" do
- get api("/projects/#{project.id}/hooks/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
end
describe "POST /projects/:id/hooks" do
diff --git a/spec/requests/api/project_import_spec.rb b/spec/requests/api/project_import_spec.rb
index e3fb6cecce9..bc06f3c3732 100644
--- a/spec/requests/api/project_import_spec.rb
+++ b/spec/requests/api/project_import_spec.rb
@@ -42,7 +42,7 @@ describe API::ProjectImport do
expect(response).to have_gitlab_http_status(201)
end
- it 'does not shedule an import for a nampespace that does not exist' do
+ it 'does not schedule an import for a namespace that does not exist' do
expect_any_instance_of(Project).not_to receive(:import_schedule)
expect(::Projects::CreateService).not_to receive(:new)
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index eb41750bf47..c249c881db5 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -20,7 +20,6 @@ describe API::Projects do
let(:admin) { create(:admin) }
let(:project) { create(:project, :repository, namespace: user.namespace) }
let(:project2) { create(:project, namespace: user.namespace) }
- let(:snippet) { create(:project_snippet, :public, author: user, project: project, title: 'example') }
let(:project_member) { create(:project_member, :developer, user: user3, project: project) }
let(:user4) { create(:user) }
let(:project3) do
@@ -575,7 +574,7 @@ describe API::Projects do
expect(json_response['avatar_url']).to eq("http://localhost/uploads/-/system/project/avatar/#{project_id}/banana_sample.gif")
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
post api('/projects', user), project
@@ -583,7 +582,7 @@ describe API::Projects do
expect(json_response['resolve_outdated_diff_discussions']).to be_falsey
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve if resolve_outdated_diff_discussions' do
+ it 'sets a project as allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: true)
post api('/projects', user), project
@@ -698,7 +697,7 @@ describe API::Projects do
expect(json_response.map { |project| project['id'] }).to contain_exactly(public_project.id)
end
- it 'returns projects filetered by minimal access level' do
+ it 'returns projects filtered by minimal access level' do
private_project1 = create(:project, :private, name: 'private_project1', creator_id: user4.id, namespace: user4.namespace)
private_project2 = create(:project, :private, name: 'private_project2', creator_id: user4.id, namespace: user4.namespace)
private_project1.add_developer(user2)
@@ -789,7 +788,7 @@ describe API::Projects do
expect(json_response['visibility']).to eq('private')
end
- it 'sets a project as allowing outdated diff discussions to automatically resolve' do
+ it 'sets a project as not allowing outdated diff discussions to automatically resolve' do
project = attributes_for(:project, resolve_outdated_diff_discussions: false)
post api("/projects/user/#{user.id}", admin), project
@@ -1119,100 +1118,6 @@ describe API::Projects do
end
end
- describe 'GET /projects/:id/snippets' do
- before do
- snippet
- end
-
- it 'returns an array of project snippets' do
- get api("/projects/#{project.id}/snippets", user)
-
- expect(response).to have_gitlab_http_status(200)
- expect(response).to include_pagination_headers
- expect(json_response).to be_an Array
- expect(json_response.first['title']).to eq(snippet.title)
- end
- end
-
- describe 'GET /projects/:id/snippets/:snippet_id' do
- it 'returns a project snippet' do
- get api("/projects/#{project.id}/snippets/#{snippet.id}", user)
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq(snippet.title)
- end
-
- it 'returns a 404 error if snippet id not found' do
- get api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
- describe 'POST /projects/:id/snippets' do
- it 'creates a new project snippet' do
- post api("/projects/#{project.id}/snippets", user),
- title: 'api test', file_name: 'sample.rb', code: 'test', visibility: 'private'
- expect(response).to have_gitlab_http_status(201)
- expect(json_response['title']).to eq('api test')
- end
-
- it 'returns a 400 error if invalid snippet is given' do
- post api("/projects/#{project.id}/snippets", user)
- expect(status).to eq(400)
- end
- end
-
- describe 'PUT /projects/:id/snippets/:snippet_id' do
- it 'updates an existing project snippet' do
- put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
- code: 'updated code'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('example')
- expect(snippet.reload.content).to eq('updated code')
- end
-
- it 'updates an existing project snippet with new title' do
- put api("/projects/#{project.id}/snippets/#{snippet.id}", user),
- title: 'other api test'
- expect(response).to have_gitlab_http_status(200)
- expect(json_response['title']).to eq('other api test')
- end
- end
-
- describe 'DELETE /projects/:id/snippets/:snippet_id' do
- before do
- snippet
- end
-
- it 'deletes existing project snippet' do
- expect do
- delete api("/projects/#{project.id}/snippets/#{snippet.id}", user)
-
- expect(response).to have_gitlab_http_status(204)
- end.to change { Snippet.count }.by(-1)
- end
-
- it 'returns 404 when deleting unknown snippet id' do
- delete api("/projects/#{project.id}/snippets/1234", user)
- expect(response).to have_gitlab_http_status(404)
- end
-
- it_behaves_like '412 response' do
- let(:request) { api("/projects/#{project.id}/snippets/#{snippet.id}", user) }
- end
- end
-
- describe 'GET /projects/:id/snippets/:snippet_id/raw' do
- it 'gets a raw project snippet' do
- get api("/projects/#{project.id}/snippets/#{snippet.id}/raw", user)
- expect(response).to have_gitlab_http_status(200)
- end
-
- it 'returns a 404 error if raw project snippet not found' do
- get api("/projects/#{project.id}/snippets/5555/raw", user)
- expect(response).to have_gitlab_http_status(404)
- end
- end
-
describe 'fork management' do
let(:project_fork_target) { create(:project) }
let(:project_fork_source) { create(:project, :public) }
@@ -1235,7 +1140,7 @@ describe API::Projects do
expect(project_fork_target.forked?).to be_truthy
end
- it 'refreshes the forks count cachce' do
+ it 'refreshes the forks count cache' do
expect(project_fork_source.forks_count).to be_zero
post api("/projects/#{project_fork_target.id}/fork/#{project_fork_source.id}", admin)
diff --git a/spec/rubocop/cop/destroy_all_spec.rb b/spec/rubocop/cop/destroy_all_spec.rb
new file mode 100644
index 00000000000..b0bc40552b3
--- /dev/null
+++ b/spec/rubocop/cop/destroy_all_spec.rb
@@ -0,0 +1,43 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/destroy_all'
+
+describe RuboCop::Cop::DestroyAll do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ it 'flags the use of destroy_all with a send receiver' do
+ inspect_source('foo.destroy_all # rubocop: disable DestroyAll')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all with a constant receiver' do
+ inspect_source('User.destroy_all # rubocop: disable DestroyAll')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all when passing arguments' do
+ inspect_source('User.destroy_all([])')
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'flags the use of destroy_all with a local variable receiver' do
+ inspect_source(<<~RUBY)
+ users = User.all
+ users.destroy_all # rubocop: disable DestroyAll
+ RUBY
+
+ expect(cop.offenses.size).to eq(1)
+ end
+
+ it 'does not flag the use of delete_all' do
+ inspect_source('foo.delete_all')
+
+ expect(cop.offenses).to be_empty
+ end
+end
diff --git a/spec/serializers/move_to_project_entity_spec.rb b/spec/serializers/move_to_project_entity_spec.rb
new file mode 100644
index 00000000000..ac495eadb68
--- /dev/null
+++ b/spec/serializers/move_to_project_entity_spec.rb
@@ -0,0 +1,19 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MoveToProjectEntity do
+ describe '#as_json' do
+ let(:project) { build(:project, id: 1) }
+
+ subject { described_class.new(project).as_json }
+
+ it 'includes the project ID' do
+ expect(subject[:id]).to eq(project.id)
+ end
+
+ it 'includes the full path' do
+ expect(subject[:name_with_namespace]).to eq(project.name_with_namespace)
+ end
+ end
+end
diff --git a/spec/serializers/move_to_project_serializer_spec.rb b/spec/serializers/move_to_project_serializer_spec.rb
new file mode 100644
index 00000000000..841ac969eeb
--- /dev/null
+++ b/spec/serializers/move_to_project_serializer_spec.rb
@@ -0,0 +1,14 @@
+# frozen_string_literal: true
+
+require 'spec_helper'
+
+describe MoveToProjectSerializer do
+ describe '#represent' do
+ it 'includes the name and name with namespace' do
+ project = build(:project, id: 1)
+ output = described_class.new.represent(project)
+
+ expect(output).to include(:id, :name_with_namespace)
+ end
+ end
+end
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/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 f4441a6b700..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.
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/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/licenses.csv b/vendor/licenses.csv
index a462daf3067..ffe56286684 100644
--- a/vendor/licenses.csv
+++ b/vendor/licenses.csv
@@ -615,7 +615,6 @@ function-bind,1.1.1,MIT
functional-red-black-tree,1.0.1,MIT
fuzzaldrin-plus,0.5.0,MIT
gauge,2.7.4,ISC
-gemnasium-gitlab-service,0.2.6,MIT
gemojione,3.3.0,MIT
generate-function,2.0.0,MIT
generate-object-property,1.2.0,MIT
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: