Welcome to mirror list, hosted at ThFree Co, Russian Federation.

gitlab.com/gitlab-org/gitlab-foss.git - Unnamed repository; edit this file 'description' to name the repository.
summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorKamil Trzcinski <ayufan@ayufan.eu>2017-06-02 21:32:02 +0300
committerKamil Trzcinski <ayufan@ayufan.eu>2017-06-02 21:32:02 +0300
commit0383b4822562005e20cdda80bca20a5fc11e9f8f (patch)
tree9f6bdaea50d256ecf58f9ccfd49198019897a196
parentb24651826f249cd1fcbd87d097c42488d0472611 (diff)
parent11852e16387790c26be7b8d1dd99ed689bf9d45b (diff)
Merge remote-tracking branch 'origin/master' into zj-job-view-goes-real-time
-rw-r--r--GITLAB_SHELL_VERSION2
-rw-r--r--Gemfile1
-rw-r--r--Gemfile.lock6
-rw-r--r--app/assets/javascripts/build.js3
-rw-r--r--app/assets/javascripts/commit/pipelines/pipelines_table.js2
-rw-r--r--app/assets/javascripts/diff_notes/components/jump_to_discussion.js7
-rw-r--r--app/assets/javascripts/droplab/keyboard.js2
-rw-r--r--app/assets/javascripts/droplab/plugins/ajax_filter.js3
-rw-r--r--app/assets/javascripts/environments/components/environment.vue97
-rw-r--r--app/assets/javascripts/environments/folder/environments_folder_view.vue100
-rw-r--r--app/assets/javascripts/environments/mixins/environments_mixin.js17
-rw-r--r--app/assets/javascripts/environments/services/environments_service.js3
-rw-r--r--app/assets/javascripts/environments/stores/environments_store.js6
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_user.js8
-rw-r--r--app/assets/javascripts/filtered_search/dropdown_utils.js5
-rw-r--r--app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js72
-rw-r--r--app/assets/javascripts/flash.js34
-rw-r--r--app/assets/javascripts/gl_field_errors.js10
-rw-r--r--app/assets/javascripts/integrations/index.js7
-rw-r--r--app/assets/javascripts/integrations/integration_settings_form.js123
-rw-r--r--app/assets/javascripts/lib/utils/url_utility.js3
-rw-r--r--app/assets/javascripts/notebook/cells/markdown.vue24
-rw-r--r--app/assets/javascripts/pipelines/components/header_component.vue97
-rw-r--r--app/assets/javascripts/pipelines/components/pipeline_url.vue2
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_bundle.js41
-rw-r--r--app/assets/javascripts/pipelines/pipeline_details_mediatior.js8
-rw-r--r--app/assets/javascripts/pipelines/pipelines.js2
-rw-r--r--app/assets/javascripts/pipelines/services/pipeline_service.js5
-rw-r--r--app/assets/javascripts/pipelines/services/pipelines_service.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/commit.js4
-rw-r--r--app/assets/javascripts/vue_shared/components/header_ci_component.vue53
-rw-r--r--app/assets/javascripts/vue_shared/components/pipelines_table_row.js2
-rw-r--r--app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue8
-rw-r--r--app/assets/stylesheets/framework/files.scss2
-rw-r--r--app/assets/stylesheets/framework/filters.scss3
-rw-r--r--app/assets/stylesheets/framework/flash.scss16
-rw-r--r--app/assets/stylesheets/framework/timeline.scss4
-rw-r--r--app/assets/stylesheets/pages/pipelines.scss8
-rw-r--r--app/controllers/admin/keys_controller.rb4
-rw-r--r--app/controllers/concerns/renders_blob.rb4
-rw-r--r--app/controllers/projects/artifacts_controller.rb2
-rw-r--r--app/controllers/projects/blob_controller.rb2
-rw-r--r--app/controllers/projects/environments_controller.rb2
-rw-r--r--app/controllers/projects/services_controller.rb39
-rw-r--r--app/controllers/projects/snippets_controller.rb2
-rw-r--r--app/controllers/projects/variables_controller.rb3
-rw-r--r--app/controllers/snippets_controller.rb2
-rw-r--r--app/helpers/avatars_helper.rb20
-rw-r--r--app/helpers/blob_helper.rb14
-rw-r--r--app/helpers/diff_helper.rb12
-rw-r--r--app/helpers/notes_helper.rb2
-rw-r--r--app/models/application_setting.rb14
-rw-r--r--app/models/audit_event.rb2
-rw-r--r--app/models/blob.rb12
-rw-r--r--app/models/blob_viewer/auxiliary.rb4
-rw-r--r--app/models/blob_viewer/base.rb26
-rw-r--r--app/models/blob_viewer/client_side.rb4
-rw-r--r--app/models/blob_viewer/server_side.rb4
-rw-r--r--app/models/blob_viewer/text.rb4
-rw-r--r--app/models/ci/build.rb38
-rw-r--r--app/models/ci/trigger_request.rb2
-rw-r--r--app/models/ci/variable.rb5
-rw-r--r--app/models/concerns/noteable.rb7
-rw-r--r--app/models/deployment.rb5
-rw-r--r--app/models/diff_discussion.rb1
-rw-r--r--app/models/diff_note.rb24
-rw-r--r--app/models/discussion.rb9
-rw-r--r--app/models/environment.rb16
-rw-r--r--app/models/event.rb2
-rw-r--r--app/models/hooks/web_hook_log.rb6
-rw-r--r--app/models/legacy_diff_note.rb2
-rw-r--r--app/models/merge_request.rb28
-rw-r--r--app/models/merge_request_diff.rb6
-rw-r--r--app/models/note.rb2
-rw-r--r--app/models/personal_access_token.rb2
-rw-r--r--app/models/project.rb13
-rw-r--r--app/models/project_import_data.rb2
-rw-r--r--app/models/project_services/asana_service.rb3
-rw-r--r--app/models/project_services/assembla_service.rb2
-rw-r--r--app/models/project_services/bamboo_service.rb4
-rw-r--r--app/models/project_services/buildkite_service.rb4
-rw-r--r--app/models/project_services/campfire_service.rb4
-rw-r--r--app/models/project_services/chat_notification_service.rb6
-rw-r--r--app/models/project_services/custom_issue_tracker_service.rb6
-rw-r--r--app/models/project_services/deployment_service.rb4
-rw-r--r--app/models/project_services/drone_ci_service.rb4
-rw-r--r--app/models/project_services/external_wiki_service.rb2
-rw-r--r--app/models/project_services/flowdock_service.rb2
-rw-r--r--app/models/project_services/gemnasium_service.rb4
-rw-r--r--app/models/project_services/hipchat_service.rb2
-rw-r--r--app/models/project_services/irker_service.rb2
-rw-r--r--app/models/project_services/issue_tracker_service.rb6
-rw-r--r--app/models/project_services/jira_service.rb12
-rw-r--r--app/models/project_services/mock_ci_service.rb7
-rw-r--r--app/models/project_services/mock_monitoring_service.rb4
-rw-r--r--app/models/project_services/pipelines_email_service.rb3
-rw-r--r--app/models/project_services/pivotaltracker_service.rb3
-rw-r--r--app/models/project_services/prometheus_service.rb3
-rw-r--r--app/models/project_services/pushover_service.rb6
-rw-r--r--app/models/project_services/teamcity_service.rb4
-rw-r--r--app/models/project_team.rb9
-rw-r--r--app/models/sent_notification.rb2
-rw-r--r--app/models/service.rb2
-rw-r--r--app/models/user.rb11
-rw-r--r--app/serializers/entity_date_helper.rb2
-rw-r--r--app/serializers/user_entity.rb5
-rw-r--r--app/services/ci/create_pipeline_service.rb9
-rw-r--r--app/services/discussions/update_diff_position_service.rb41
-rw-r--r--app/services/gravatar_service.rb21
-rw-r--r--app/services/merge_requests/conflicts/resolve_service.rb12
-rw-r--r--app/services/merge_requests/create_service.rb9
-rw-r--r--app/services/notes/diff_position_update_service.rb33
-rw-r--r--app/uploaders/artifact_uploader.rb28
-rw-r--r--app/uploaders/gitlab_uploader.rb6
-rw-r--r--app/validators/dynamic_path_validator.rb2
-rw-r--r--app/views/admin/dashboard/index.html.haml6
-rw-r--r--app/views/admin/users/_user.html.haml2
-rw-r--r--app/views/discussions/_jump_to_next.html.haml4
-rw-r--r--app/views/projects/blame/show.html.haml2
-rw-r--r--app/views/projects/blob/_breadcrumb.html.haml2
-rw-r--r--app/views/projects/diffs/_content.html.haml2
-rw-r--r--app/views/projects/diffs/_diffs.html.haml4
-rw-r--r--app/views/projects/diffs/_line.html.haml2
-rw-r--r--app/views/projects/jobs/_sidebar.html.haml3
-rw-r--r--app/views/projects/pipelines/_info.html.haml16
-rw-r--r--app/views/projects/pipelines_settings/_show.html.haml2
-rw-r--r--app/views/projects/registry/repositories/index.html.haml72
-rw-r--r--app/views/projects/services/_form.html.haml13
-rw-r--r--app/views/projects/variables/_content.html.haml5
-rw-r--r--app/views/projects/variables/_form.html.haml9
-rw-r--r--app/views/projects/variables/_table.html.haml3
-rw-r--r--app/views/shared/_field.html.haml7
-rw-r--r--app/views/shared/issuable/_search_bar.html.haml33
-rw-r--r--app/views/shared/issuable/_user_dropdown_item.html.haml11
-rw-r--r--changelogs/unreleased/10378-promote-blameless-culture.yml4
-rw-r--r--changelogs/unreleased/24196-protected-variables.yml5
-rw-r--r--changelogs/unreleased/28080-system-checks.yml4
-rw-r--r--changelogs/unreleased/30651-improve-container-registry-description.yml4
-rw-r--r--changelogs/unreleased/31511-jira-settings.yml4
-rw-r--r--changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml4
-rw-r--r--changelogs/unreleased/31644-make-cookie-sessions-unique.yml4
-rw-r--r--changelogs/unreleased/31849-pipeline-real-time-header.yml4
-rw-r--r--changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml4
-rw-r--r--changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml4
-rw-r--r--changelogs/unreleased/33215-fix-hard-delete-of-users.yml4
-rw-r--r--changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml4
-rw-r--r--changelogs/unreleased/aliyun-backup-provider.yml4
-rw-r--r--changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml4
-rw-r--r--changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml4
-rw-r--r--changelogs/unreleased/dm-discussions-n-plus-1.yml4
-rw-r--r--changelogs/unreleased/dm-emails-are-not-user-references.yml4
-rw-r--r--changelogs/unreleased/dm-fix-jump-button.yml4
-rw-r--r--changelogs/unreleased/dm-gravatar-username.yml4
-rw-r--r--changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml4
-rw-r--r--changelogs/unreleased/fix_diff_line_comments.yml5
-rw-r--r--changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml5
-rw-r--r--changelogs/unreleased/migrate-artifacts-to-a-new-path.yml4
-rw-r--r--changelogs/unreleased/winh-current-user-filter.yml4
-rw-r--r--changelogs/unreleased/winh-styled-people-search-bar.yml4
-rw-r--r--changelogs/unreleased/zj-drop-fk-if-exists.yml4
-rw-r--r--changelogs/unreleased/zj-realtime-env-list.yml4
-rw-r--r--config/gitlab.yml.example2
-rw-r--r--config/initializers/active_record_locking.rb (renamed from config/initializers/ar_monkey_patch.rb)0
-rw-r--r--config/initializers/active_record_preloader.rb15
-rw-r--r--config/initializers/session_store.rb8
-rw-r--r--config/karma.config.js2
-rw-r--r--config/locales/en.yml36
-rw-r--r--config/locales/es.yml35
-rw-r--r--config/routes/project.rb2
-rw-r--r--config/webpack.config.js3
-rw-r--r--db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb12
-rw-r--r--db/migrate/20170524161101_add_protected_to_ci_variables.rb15
-rw-r--r--db/post_migrate/20170523083112_migrate_old_artifacts.rb72
-rw-r--r--db/schema.rb3
-rw-r--r--doc/api/build_variables.md28
-rw-r--r--doc/ci/variables/README.md24
-rw-r--r--doc/customization/libravatar.md4
-rw-r--r--doc/development/README.md1
-rw-r--r--doc/development/i18n_guide.md3
-rw-r--r--doc/development/serializing_data.md84
-rw-r--r--doc/install/installation.md4
-rw-r--r--doc/raketasks/backup_restore.md2
-rw-r--r--doc/user/project/container_registry.md2
-rw-r--r--doc/user/project/img/container_registry_panel.pngbin32310 -> 0 bytes
-rw-r--r--doc/user/project/pipelines/schedules.md2
-rw-r--r--doc/workflow/gitlab_flow.md12
-rw-r--r--features/project/service.feature26
-rw-r--r--features/steps/project/services.rb70
-rw-r--r--features/steps/project/source/browse_files.rb1
-rw-r--r--lib/api/commits.rb2
-rw-r--r--lib/api/entities.rb7
-rw-r--r--lib/api/helpers.rb10
-rw-r--r--lib/api/jobs.rb10
-rw-r--r--lib/api/projects.rb1
-rw-r--r--lib/api/runner.rb11
-rw-r--r--lib/api/v3/builds.rb10
-rw-r--r--lib/api/v3/commits.rb2
-rw-r--r--lib/api/v3/deploy_keys.rb1
-rw-r--r--lib/api/v3/entities.rb2
-rw-r--r--lib/api/variables.rb4
-rw-r--r--lib/backup/artifacts.rb2
-rw-r--r--lib/ci/api/builds.rb8
-rw-r--r--lib/gitlab/current_settings.rb5
-rw-r--r--lib/gitlab/diff/file_collection/base.rb2
-rw-r--r--lib/gitlab/diff/line.rb4
-rw-r--r--lib/gitlab/email/message/repository_push.rb2
-rw-r--r--lib/gitlab/encoding_helper.rb62
-rw-r--r--lib/gitlab/etag_caching/router.rb8
-rw-r--r--lib/gitlab/git/blame.rb2
-rw-r--r--lib/gitlab/git/blob.rb3
-rw-r--r--lib/gitlab/git/commit.rb2
-rw-r--r--lib/gitlab/git/diff.rb61
-rw-r--r--lib/gitlab/git/diff_collection.rb20
-rw-r--r--lib/gitlab/git/encoding_helper.rb64
-rw-r--r--lib/gitlab/git/ref.rb2
-rw-r--r--lib/gitlab/git/tree.rb2
-rw-r--r--lib/gitlab/utils.rb8
-rw-r--r--lib/system_check.rb21
-rw-r--r--lib/system_check/app/active_users_check.rb17
-rw-r--r--lib/system_check/app/database_config_exists_check.rb25
-rw-r--r--lib/system_check/app/git_config_check.rb42
-rw-r--r--lib/system_check/app/git_version_check.rb29
-rw-r--r--lib/system_check/app/gitlab_config_exists_check.rb24
-rw-r--r--lib/system_check/app/gitlab_config_up_to_date_check.rb30
-rw-r--r--lib/system_check/app/init_script_exists_check.rb27
-rw-r--r--lib/system_check/app/init_script_up_to_date_check.rb43
-rw-r--r--lib/system_check/app/log_writable_check.rb28
-rw-r--r--lib/system_check/app/migrations_are_up_check.rb20
-rw-r--r--lib/system_check/app/orphaned_group_members_check.rb20
-rw-r--r--lib/system_check/app/projects_have_namespace_check.rb37
-rw-r--r--lib/system_check/app/redis_version_check.rb25
-rw-r--r--lib/system_check/app/ruby_version_check.rb27
-rw-r--r--lib/system_check/app/tmp_writable_check.rb28
-rw-r--r--lib/system_check/app/uploads_directory_exists_check.rb21
-rw-r--r--lib/system_check/app/uploads_path_permission_check.rb36
-rw-r--r--lib/system_check/app/uploads_path_tmp_permission_check.rb40
-rw-r--r--lib/system_check/base_check.rb129
-rw-r--r--lib/system_check/helpers.rb75
-rw-r--r--lib/system_check/simple_executor.rb99
-rw-r--r--lib/tasks/gettext.rake8
-rw-r--r--lib/tasks/gitlab/check.rake494
-rw-r--r--lib/tasks/gitlab/task_helpers.rb44
-rw-r--r--rubocop/cop/activerecord_serialize.rb24
-rw-r--r--rubocop/rubocop.rb1
-rw-r--r--spec/controllers/projects/environments_controller_spec.rb5
-rw-r--r--spec/controllers/projects/merge_requests_controller_spec.rb2
-rw-r--r--spec/controllers/projects/services_controller_spec.rb112
-rw-r--r--spec/db/production/settings.rb1
-rw-r--r--spec/factories/ci/variables.rb4
-rw-r--r--spec/factories/services.rb6
-rw-r--r--spec/features/admin/admin_users_spec.rb2
-rw-r--r--spec/features/boards/modal_filter_spec.rb4
-rw-r--r--spec/features/commits_spec.rb20
-rw-r--r--spec/features/container_registry_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/dropdown_assignee_spec.rb19
-rw-r--r--spec/features/issues/filtered_search/dropdown_author_spec.rb19
-rw-r--r--spec/features/issues/filtered_search/filter_issues_spec.rb2
-rw-r--r--spec/features/issues/filtered_search/visual_tokens_spec.rb4
-rw-r--r--spec/features/merge_requests/discussion_spec.rb41
-rw-r--r--spec/features/projects/blobs/blob_line_permalink_updater_spec.rb2
-rw-r--r--spec/features/projects/files/browse_files_spec.rb2
-rw-r--r--spec/features/projects/pipelines/pipeline_spec.rb2
-rw-r--r--spec/features/projects/services/jira_service_spec.rb92
-rw-r--r--spec/features/projects/services/mattermost_slash_command_spec.rb18
-rw-r--r--spec/features/projects/services/slack_slash_command_spec.rb14
-rw-r--r--spec/features/variables_spec.rb48
-rw-r--r--spec/helpers/avatars_helper_spec.rb101
-rw-r--r--spec/helpers/blob_helper_spec.rb28
-rw-r--r--spec/helpers/diff_helper_spec.rb37
-rw-r--r--spec/javascripts/droplab/plugins/ajax_filter_spec.js72
-rw-r--r--spec/javascripts/environments/environments_store_spec.js9
-rw-r--r--spec/javascripts/filtered_search/dropdown_utils_spec.js29
-rw-r--r--spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js406
-rw-r--r--spec/javascripts/fixtures/issues.rb11
-rw-r--r--spec/javascripts/fixtures/raw.rb6
-rw-r--r--spec/javascripts/fixtures/services.rb31
-rw-r--r--spec/javascripts/helpers/filtered_search_spec_helper.js13
-rw-r--r--spec/javascripts/integrations/integration_settings_form_spec.js199
-rw-r--r--spec/javascripts/notebook/cells/markdown_spec.js57
-rw-r--r--spec/javascripts/pipelines/header_component_spec.js60
-rw-r--r--spec/javascripts/pipelines/pipeline_details_mediator_spec.js41
-rw-r--r--spec/javascripts/pipelines/pipeline_store_spec.js27
-rw-r--r--spec/javascripts/pipelines/pipeline_url_spec.js1
-rw-r--r--spec/javascripts/vue_shared/components/commit_spec.js4
-rw-r--r--spec/javascripts/vue_shared/components/header_ci_component_spec.js11
-rw-r--r--spec/javascripts/vue_shared/components/pipelines_table_row_spec.js4
-rw-r--r--spec/lib/banzai/filter/user_reference_filter_spec.rb5
-rw-r--r--spec/lib/gitlab/encoding_helper_spec.rb (renamed from spec/lib/gitlab/git/encoding_helper_spec.rb)4
-rw-r--r--spec/lib/gitlab/etag_caching/router_spec.rb11
-rw-r--r--spec/lib/gitlab/git/diff_collection_spec.rb26
-rw-r--r--spec/lib/gitlab/git/diff_spec.rb24
-rw-r--r--spec/lib/gitlab/git/repository_spec.rb2
-rw-r--r--spec/lib/gitlab/utils_spec.rb11
-rw-r--r--spec/lib/system_check/simple_executor_spec.rb223
-rw-r--r--spec/lib/system_check_spec.rb36
-rw-r--r--spec/migrations/migrate_old_artifacts_spec.rb117
-rw-r--r--spec/models/blob_viewer/base_spec.rb70
-rw-r--r--spec/models/ci/build_spec.rb68
-rw-r--r--spec/models/ci/variable_spec.rb33
-rw-r--r--spec/models/deployment_spec.rb13
-rw-r--r--spec/models/diff_note_spec.rb27
-rw-r--r--spec/models/environment_spec.rb22
-rw-r--r--spec/models/merge_request_spec.rb14
-rw-r--r--spec/models/project_services/jira_service_spec.rb35
-rw-r--r--spec/models/project_spec.rb84
-rw-r--r--spec/models/project_team_spec.rb151
-rw-r--r--spec/models/user_spec.rb2
-rw-r--r--spec/requests/api/projects_spec.rb26
-rw-r--r--spec/requests/api/v3/deploy_keys_spec.rb9
-rw-r--r--spec/requests/api/variables_spec.rb7
-rw-r--r--spec/rubocop/cop/activerecord_serialize_spec.rb33
-rw-r--r--spec/serializers/pipeline_serializer_spec.rb2
-rw-r--r--spec/serializers/user_entity_spec.rb6
-rw-r--r--spec/services/ci/create_pipeline_service_spec.rb5
-rw-r--r--spec/services/discussions/update_diff_position_service_spec.rb (renamed from spec/services/notes/diff_position_update_service_spec.rb)28
-rw-r--r--spec/services/gravatar_service_spec.rb20
-rw-r--r--spec/services/merge_requests/conflicts/resolve_service_spec.rb42
-rw-r--r--spec/services/users/destroy_service_spec.rb8
-rw-r--r--spec/services/wiki_pages/create_service_spec.rb40
-rw-r--r--spec/services/wiki_pages/destroy_service_spec.rb19
-rw-r--r--spec/services/wiki_pages/update_service_spec.rb42
-rw-r--r--spec/spec_helper.rb3
-rw-r--r--spec/support/matchers/execute_check.rb23
-rw-r--r--spec/support/rake_helpers.rb5
-rw-r--r--spec/support/test_env.rb2
-rw-r--r--spec/uploaders/artifact_uploader_spec.rb38
-rw-r--r--spec/uploaders/gitlab_uploader_spec.rb56
-rw-r--r--spec/views/projects/blob/_viewer.html.haml_spec.rb4
328 files changed, 5166 insertions, 1735 deletions
diff --git a/GITLAB_SHELL_VERSION b/GITLAB_SHELL_VERSION
index 2d6c0bcf19c..ab0fa336dd0 100644
--- a/GITLAB_SHELL_VERSION
+++ b/GITLAB_SHELL_VERSION
@@ -1 +1 @@
-5.0.4
+5.0.5
diff --git a/Gemfile b/Gemfile
index dce2e4ba94e..c49b60ffc23 100644
--- a/Gemfile
+++ b/Gemfile
@@ -97,6 +97,7 @@ gem 'fog-google', '~> 0.5'
gem 'fog-local', '~> 0.3'
gem 'fog-openstack', '~> 0.1'
gem 'fog-rackspace', '~> 0.1.1'
+gem 'fog-aliyun', '~> 0.1.0'
# for Google storage
gem 'google-api-client', '~> 0.8.6'
diff --git a/Gemfile.lock b/Gemfile.lock
index f0728a358fa..f8adfec6143 100644
--- a/Gemfile.lock
+++ b/Gemfile.lock
@@ -213,6 +213,11 @@ GEM
flowdock (0.7.1)
httparty (~> 0.7)
multi_json
+ fog-aliyun (0.1.0)
+ fog-core (~> 1.27)
+ fog-json (~> 1.0)
+ ipaddress (~> 0.8)
+ xml-simple (~> 1.1)
fog-aws (0.13.0)
fog-core (~> 1.38)
fog-json (~> 1.0)
@@ -913,6 +918,7 @@ DEPENDENCIES
flay (~> 2.8.0)
flipper (~> 0.10.2)
flipper-active_record (~> 0.10.2)
+ fog-aliyun (~> 0.1.0)
fog-aws (~> 0.9)
fog-core (~> 1.44)
fog-google (~> 0.5)
diff --git a/app/assets/javascripts/build.js b/app/assets/javascripts/build.js
index 1a602cbd8a7..072a899e9f2 100644
--- a/app/assets/javascripts/build.js
+++ b/app/assets/javascripts/build.js
@@ -64,7 +64,7 @@ window.Build = (function () {
$(window)
.off('resize.build')
- .on('resize.build', this.sidebarOnResize.bind(this));
+ .on('resize.build', _.throttle(this.sidebarOnResize.bind(this), 100));
this.updateArtifactRemoveDate();
@@ -250,6 +250,7 @@ window.Build = (function () {
Build.prototype.sidebarOnResize = function () {
this.toggleSidebar(this.shouldHideSidebarForViewport());
+
this.verifyTopPosition();
if (this.$scrollContainer.getNiceScroll(0)) {
diff --git a/app/assets/javascripts/commit/pipelines/pipelines_table.js b/app/assets/javascripts/commit/pipelines/pipelines_table.js
index 98698143d22..082fbafb740 100644
--- a/app/assets/javascripts/commit/pipelines/pipelines_table.js
+++ b/app/assets/javascripts/commit/pipelines/pipelines_table.js
@@ -118,7 +118,7 @@ export default Vue.component('pipelines-table', {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeDestroyed() {
+ beforeDestroy() {
eventHub.$off('refreshPipelines');
},
diff --git a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
index 8a0fd3bb4a7..37ddca29e71 100644
--- a/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
+++ b/app/assets/javascripts/diff_notes/components/jump_to_discussion.js
@@ -16,6 +16,13 @@ const JumpToDiscussion = Vue.extend({
};
},
computed: {
+ buttonText: function () {
+ if (this.discussionId) {
+ return 'Jump to next unresolved discussion';
+ } else {
+ return 'Jump to first unresolved discussion';
+ }
+ },
allResolved: function () {
return this.unresolvedDiscussionCount === 0;
},
diff --git a/app/assets/javascripts/droplab/keyboard.js b/app/assets/javascripts/droplab/keyboard.js
index 36740a430e1..02f1b805ce4 100644
--- a/app/assets/javascripts/droplab/keyboard.js
+++ b/app/assets/javascripts/droplab/keyboard.js
@@ -8,7 +8,7 @@ const Keyboard = function () {
var isUpArrow = false;
var isDownArrow = false;
var removeHighlight = function removeHighlight(list) {
- var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider)'), 0);
+ var itemElements = Array.prototype.slice.call(list.list.querySelectorAll('li:not(.divider):not(.hidden)'), 0);
var listItems = [];
for(var i = 0; i < itemElements.length; i++) {
var listItem = itemElements[i];
diff --git a/app/assets/javascripts/droplab/plugins/ajax_filter.js b/app/assets/javascripts/droplab/plugins/ajax_filter.js
index a5427417031..1db20227a16 100644
--- a/app/assets/javascripts/droplab/plugins/ajax_filter.js
+++ b/app/assets/javascripts/droplab/plugins/ajax_filter.js
@@ -63,6 +63,9 @@ const AjaxFilter = {
return AjaxCache.retrieve(url)
.then((data) => {
this._loadData(data, config);
+ if (config.onLoadingFinished) {
+ config.onLoadingFinished(data);
+ }
})
.catch(config.onError);
},
diff --git a/app/assets/javascripts/environments/components/environment.vue b/app/assets/javascripts/environments/components/environment.vue
index d4e13f3c84a..c9e489dd90e 100644
--- a/app/assets/javascripts/environments/components/environment.vue
+++ b/app/assets/javascripts/environments/components/environment.vue
@@ -1,5 +1,6 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from './environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
@@ -7,6 +8,8 @@ import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
import '../../lib/utils/common_utils';
import eventHub from '../event_hub';
+import Poll from '../../lib/utils/poll';
+import environmentsMixin from '../mixins/environments_mixin';
export default {
@@ -16,6 +19,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-list-view').dataset;
const store = new EnvironmentsStore();
@@ -35,6 +42,7 @@ export default {
projectStoppedEnvironmentsPath: environmentsData.projectStoppedEnvironmentsPath,
newEnvironmentPath: environmentsData.newEnvironmentPath,
helpPagePath: environmentsData.helpPagePath,
+ isMakingRequest: false,
// Pagination Properties,
paginationInformation: {},
@@ -65,17 +73,43 @@ export default {
* Toggles loading property.
*/
created() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
this.service = new EnvironmentsService(this.endpoint);
- this.fetchEnvironments();
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+
+ // We need to verify if any folder is open to also fecth it
+ this.openFolders = this.store.getOpenFolders();
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
- eventHub.$on('refreshEnvironments', this.fetchEnvironments);
eventHub.$on('toggleFolder', this.toggleFolder);
eventHub.$on('postAction', this.postAction);
},
- beforeDestroyed() {
- eventHub.$off('refreshEnvironments');
+ beforeDestroy() {
eventHub.$off('toggleFolder');
eventHub.$off('postAction');
},
@@ -104,29 +138,13 @@ export default {
fetchEnvironments() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
this.isLoading = true;
- return this.service.get(scope, pageNumber)
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.');
- });
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
},
fetchChildEnvironments(folder, folderUrl) {
@@ -146,9 +164,34 @@ export default {
},
postAction(endpoint) {
- this.service.postAction(endpoint)
- .then(() => this.fetchEnvironments())
- .catch(() => new Flash('An error occured while making the request.'));
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+
+ // If folders are open while polling we need to open them again
+ if (this.openFolders.length) {
+ this.openFolders.map((folder) => {
+ // TODO - Move this to the backend
+ const folderUrl = `${window.location.pathname}/folders/${folder.folderName}`;
+
+ this.store.updateFolder(folder, 'isOpen', true);
+ return this.fetchChildEnvironments(folder, folderUrl);
+ });
+ }
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
},
},
};
diff --git a/app/assets/javascripts/environments/folder/environments_folder_view.vue b/app/assets/javascripts/environments/folder/environments_folder_view.vue
index bd161c8a379..925503a01c4 100644
--- a/app/assets/javascripts/environments/folder/environments_folder_view.vue
+++ b/app/assets/javascripts/environments/folder/environments_folder_view.vue
@@ -1,12 +1,15 @@
<script>
/* global Flash */
+import Visibility from 'visibilityjs';
import EnvironmentsService from '../services/environments_service';
import environmentTable from '../components/environments_table.vue';
import EnvironmentsStore from '../stores/environments_store';
import loadingIcon from '../../vue_shared/components/loading_icon.vue';
import tablePagination from '../../vue_shared/components/table_pagination.vue';
+import Poll from '../../lib/utils/poll';
+import eventHub from '../event_hub';
+import environmentsMixin from '../mixins/environments_mixin';
import '../../lib/utils/common_utils';
-import '../../vue_shared/vue_resource_interceptor';
export default {
components: {
@@ -15,6 +18,10 @@ export default {
loadingIcon,
},
+ mixins: [
+ environmentsMixin,
+ ],
+
data() {
const environmentsData = document.querySelector('#environments-folder-list-view').dataset;
const store = new EnvironmentsStore();
@@ -76,33 +83,39 @@ export default {
*/
created() {
const scope = gl.utils.getParameterByName('scope') || this.visibility;
- const pageNumber = gl.utils.getParameterByName('page') || this.pageNumber;
-
- const endpoint = `${this.endpoint}?scope=${scope}&page=${pageNumber}`;
-
- this.service = new EnvironmentsService(endpoint);
-
- this.isLoading = true;
-
- return this.service.get()
- .then(resp => ({
- headers: resp.headers,
- body: resp.json(),
- }))
- .then((response) => {
- this.store.storeAvailableCount(response.body.available_count);
- this.store.storeStoppedCount(response.body.stopped_count);
- this.store.storeEnvironments(response.body.environments);
- this.store.setPagination(response.headers);
- })
- .then(() => {
- this.isLoading = false;
- })
- .catch(() => {
- this.isLoading = false;
- // eslint-disable-next-line no-new
- new Flash('An error occurred while fetching the environments.', 'alert');
- });
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.service = new EnvironmentsService(this.endpoint);
+
+ const poll = new Poll({
+ resource: this.service,
+ method: 'get',
+ data: { scope, page },
+ successCallback: this.successCallback,
+ errorCallback: this.errorCallback,
+ notificationCallback: (isMakingRequest) => {
+ this.isMakingRequest = isMakingRequest;
+ },
+ });
+
+ if (!Visibility.hidden()) {
+ this.isLoading = true;
+ poll.makeRequest();
+ }
+
+ Visibility.change(() => {
+ if (!Visibility.hidden()) {
+ poll.restart();
+ } else {
+ poll.stop();
+ }
+ });
+
+ eventHub.$on('postAction', this.postAction);
+ },
+
+ beforeDestroyed() {
+ eventHub.$off('postAction');
},
methods: {
@@ -117,6 +130,37 @@ export default {
gl.utils.visitUrl(param);
return param;
},
+
+ fetchEnvironments() {
+ const scope = gl.utils.getParameterByName('scope') || this.visibility;
+ const page = gl.utils.getParameterByName('page') || this.pageNumber;
+
+ this.isLoading = true;
+
+ return this.service.get({ scope, page })
+ .then(this.successCallback)
+ .catch(this.errorCallback);
+ },
+
+ successCallback(resp) {
+ this.saveData(resp);
+ },
+
+ errorCallback() {
+ this.isLoading = false;
+ // eslint-disable-next-line no-new
+ new Flash('An error occurred while fetching the environments.');
+ },
+
+ postAction(endpoint) {
+ if (!this.isMakingRequest) {
+ this.isLoading = true;
+
+ this.service.postAction(endpoint)
+ .then(() => this.fetchEnvironments())
+ .catch(() => new Flash('An error occured while making the request.'));
+ }
+ },
},
};
</script>
diff --git a/app/assets/javascripts/environments/mixins/environments_mixin.js b/app/assets/javascripts/environments/mixins/environments_mixin.js
new file mode 100644
index 00000000000..25b24fbd6dc
--- /dev/null
+++ b/app/assets/javascripts/environments/mixins/environments_mixin.js
@@ -0,0 +1,17 @@
+export default {
+ methods: {
+ saveData(resp) {
+ const response = {
+ headers: resp.headers,
+ body: resp.json(),
+ };
+
+ this.isLoading = false;
+
+ this.store.storeAvailableCount(response.body.available_count);
+ this.store.storeStoppedCount(response.body.stopped_count);
+ this.store.storeEnvironments(response.body.environments);
+ this.store.setPagination(response.headers);
+ },
+ },
+};
diff --git a/app/assets/javascripts/environments/services/environments_service.js b/app/assets/javascripts/environments/services/environments_service.js
index 8adb53ea86d..03ab74b3338 100644
--- a/app/assets/javascripts/environments/services/environments_service.js
+++ b/app/assets/javascripts/environments/services/environments_service.js
@@ -10,7 +10,8 @@ export default class EnvironmentsService {
this.folderResults = 3;
}
- get(scope, page) {
+ get(options = {}) {
+ const { scope, page } = options;
return this.environments.get({ scope, page });
}
diff --git a/app/assets/javascripts/environments/stores/environments_store.js b/app/assets/javascripts/environments/stores/environments_store.js
index 158e7922e3c..8a2f6a473de 100644
--- a/app/assets/javascripts/environments/stores/environments_store.js
+++ b/app/assets/javascripts/environments/stores/environments_store.js
@@ -153,4 +153,10 @@ export default class EnvironmentsStore {
return updatedEnvironments;
}
+ getOpenFolders() {
+ const environments = this.state.environments;
+
+ return environments.filter(env => env.isFolder && env.isOpen);
+ }
+
}
diff --git a/app/assets/javascripts/filtered_search/dropdown_user.js b/app/assets/javascripts/filtered_search/dropdown_user.js
index 6b4338ca1d6..65c1b2050ac 100644
--- a/app/assets/javascripts/filtered_search/dropdown_user.js
+++ b/app/assets/javascripts/filtered_search/dropdown_user.js
@@ -18,6 +18,9 @@ class DropdownUser extends gl.FilteredSearchDropdown {
},
searchValueFunction: this.getSearchInput.bind(this),
loadingTemplate: this.loadingTemplate,
+ onLoadingFinished: () => {
+ this.hideCurrentUser();
+ },
onError() {
/* eslint-disable no-new */
new Flash('An error occured fetching the dropdown data.');
@@ -28,6 +31,11 @@ class DropdownUser extends gl.FilteredSearchDropdown {
this.tokenKeys = tokenKeys;
}
+ hideCurrentUser() {
+ const currenUserItem = this.dropdown.querySelector('.js-current-user');
+ currenUserItem.classList.add('hidden');
+ }
+
itemClicked(e) {
super.itemClicked(e,
selected => selected.querySelector('.dropdown-light-content').innerText.trim());
diff --git a/app/assets/javascripts/filtered_search/dropdown_utils.js b/app/assets/javascripts/filtered_search/dropdown_utils.js
index 5c02a7a53d3..ef8fe071012 100644
--- a/app/assets/javascripts/filtered_search/dropdown_utils.js
+++ b/app/assets/javascripts/filtered_search/dropdown_utils.js
@@ -102,10 +102,13 @@ class DropdownUtils {
if (token.classList.contains('js-visual-token')) {
const name = token.querySelector('.name');
const value = token.querySelector('.value');
+ const valueContainer = token.querySelector('.value-container');
const symbol = value && value.dataset.symbol ? value.dataset.symbol : '';
let valueText = '';
- if (value && value.innerText) {
+ if (valueContainer && valueContainer.dataset.originalValue) {
+ valueText = valueContainer.dataset.originalValue;
+ } else if (value && value.innerText) {
valueText = value.innerText;
}
diff --git a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
index bc1226f5879..e9278140af0 100644
--- a/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
+++ b/app/assets/javascripts/filtered_search/filtered_search_visual_tokens.js
@@ -1,6 +1,7 @@
-import AjaxCache from '~/lib/utils/ajax_cache';
-import '~/flash'; /* global Flash */
+import AjaxCache from '../lib/utils/ajax_cache';
+import '../flash'; /* global Flash */
import FilteredSearchContainer from './container';
+import UsersCache from '../lib/utils/users_cache';
class FilteredSearchVisualTokens {
static getLastVisualTokenBeforeInput() {
@@ -82,12 +83,42 @@ class FilteredSearchVisualTokens {
.catch(() => new Flash('An error occurred while fetching label colors.'));
}
+ static updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue) {
+ if (tokenValue === 'none') {
+ return Promise.resolve();
+ }
+
+ const username = tokenValue.replace(/^@/, '');
+ return UsersCache.retrieve(username)
+ .then((user) => {
+ if (!user) {
+ return;
+ }
+
+ /* eslint-disable no-param-reassign */
+ tokenValueContainer.dataset.originalValue = tokenValue;
+ tokenValueElement.innerHTML = `
+ <img class="avatar s20" src="${user.avatar_url}" alt="${user.name}'s avatar">
+ ${user.name}
+ `;
+ /* eslint-enable no-param-reassign */
+ })
+ // ignore error and leave username in the search bar
+ .catch(() => { });
+ }
+
static renderVisualTokenValue(parentElement, tokenName, tokenValue) {
const tokenValueContainer = parentElement.querySelector('.value-container');
- tokenValueContainer.querySelector('.value').innerText = tokenValue;
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ tokenValueElement.innerText = tokenValue;
- if (tokenName.toLowerCase() === 'label') {
+ const tokenType = tokenName.toLowerCase();
+ if (tokenType === 'label') {
FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue);
+ } else if ((tokenType === 'author') || (tokenType === 'assignee')) {
+ FilteredSearchVisualTokens.updateUserTokenAppearance(
+ tokenValueContainer, tokenValueElement, tokenValue,
+ );
}
}
@@ -153,6 +184,12 @@ class FilteredSearchVisualTokens {
if (!lastVisualToken) return '';
+ const valueContainer = lastVisualToken.querySelector('.value-container');
+ const originalValue = valueContainer && valueContainer.dataset.originalValue;
+ if (originalValue) {
+ return originalValue;
+ }
+
const value = lastVisualToken.querySelector('.value');
const name = lastVisualToken.querySelector('.name');
@@ -205,17 +242,28 @@ class FilteredSearchVisualTokens {
const inputLi = input.parentElement;
tokenContainer.replaceChild(inputLi, token);
- const name = token.querySelector('.name');
- const value = token.querySelector('.value');
+ const nameElement = token.querySelector('.name');
+ let value;
- if (token.classList.contains('filtered-search-token') && value) {
- FilteredSearchVisualTokens.addFilterVisualToken(name.innerText);
- input.value = value.innerText;
- } else {
- // token is a search term
- input.value = name.innerText;
+ if (token.classList.contains('filtered-search-token')) {
+ FilteredSearchVisualTokens.addFilterVisualToken(nameElement.innerText);
+
+ const valueContainerElement = token.querySelector('.value-container');
+ value = valueContainerElement.dataset.originalValue;
+
+ if (!value) {
+ const valueElement = valueContainerElement.querySelector('.value');
+ value = valueElement.innerText;
+ }
}
+ // token is a search term
+ if (!value) {
+ value = nameElement.innerText;
+ }
+
+ input.value = value;
+
// Opens dropdown
const inputEvent = new Event('input');
input.dispatchEvent(inputEvent);
diff --git a/app/assets/javascripts/flash.js b/app/assets/javascripts/flash.js
index eec30624ff2..ccff8f0ace7 100644
--- a/app/assets/javascripts/flash.js
+++ b/app/assets/javascripts/flash.js
@@ -7,8 +7,21 @@ window.Flash = (function() {
return $(this).fadeOut();
};
- function Flash(message, type, parent) {
- var flash, textDiv;
+ /**
+ * Flash banner supports different types of Flash configurations
+ * along with ability to provide actionConfig which can be used to show
+ * additional action or link on banner next to message
+ *
+ * @param {String} message Flash message
+ * @param {String} type Type of Flash, it can be `notice` or `alert` (default)
+ * @param {Object} parent Reference to Parent element under which Flash needs to appear
+ * @param {Object} actionConfig Map of config to show action on banner
+ * @param {String} href URL to which action link should point (default '#')
+ * @param {String} title Title of action
+ * @param {Function} clickHandler Method to call when action is clicked on
+ */
+ function Flash(message, type, parent, actionConfig) {
+ var flash, textDiv, actionLink;
if (type == null) {
type = 'alert';
}
@@ -30,6 +43,23 @@ window.Flash = (function() {
text: message
});
textDiv.appendTo(flash);
+
+ if (actionConfig) {
+ const actionLinkConfig = {
+ class: 'flash-action',
+ href: actionConfig.href || '#',
+ text: actionConfig.title
+ };
+
+ if (!actionConfig.href) {
+ actionLinkConfig.role = 'button';
+ }
+
+ actionLink = $('<a/>', actionLinkConfig);
+
+ actionLink.appendTo(flash);
+ this.flashContainer.on('click', '.flash-action', actionConfig.clickHandler);
+ }
if (this.flashContainer.parent().hasClass('content-wrapper')) {
textDiv.addClass('container-fluid container-limited');
}
diff --git a/app/assets/javascripts/gl_field_errors.js b/app/assets/javascripts/gl_field_errors.js
index 4f226ff96ea..4bef60264bb 100644
--- a/app/assets/javascripts/gl_field_errors.js
+++ b/app/assets/javascripts/gl_field_errors.js
@@ -31,9 +31,13 @@ class GlFieldErrors {
* and prevents disabling of invalid submit button by application.js */
catchInvalidFormSubmit (event) {
- if (!event.currentTarget.checkValidity()) {
- event.preventDefault();
- event.stopPropagation();
+ const $form = $(event.currentTarget);
+
+ if (!$form.attr('novalidate')) {
+ if (!event.currentTarget.checkValidity()) {
+ event.preventDefault();
+ event.stopPropagation();
+ }
}
}
diff --git a/app/assets/javascripts/integrations/index.js b/app/assets/javascripts/integrations/index.js
new file mode 100644
index 00000000000..10fe6bac0e8
--- /dev/null
+++ b/app/assets/javascripts/integrations/index.js
@@ -0,0 +1,7 @@
+/* eslint-disable no-new */
+import IntegrationSettingsForm from './integration_settings_form';
+
+$(() => {
+ const integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ integrationSettingsForm.init();
+});
diff --git a/app/assets/javascripts/integrations/integration_settings_form.js b/app/assets/javascripts/integrations/integration_settings_form.js
new file mode 100644
index 00000000000..ddd3a6aab99
--- /dev/null
+++ b/app/assets/javascripts/integrations/integration_settings_form.js
@@ -0,0 +1,123 @@
+/* global Flash */
+
+export default class IntegrationSettingsForm {
+ constructor(formSelector) {
+ this.$form = $(formSelector);
+
+ // Form Metadata
+ this.canTestService = this.$form.data('can-test');
+ this.testEndPoint = this.$form.data('test-url');
+
+ // Form Child Elements
+ this.$serviceToggle = this.$form.find('#service_active');
+ this.$submitBtn = this.$form.find('button[type="submit"]');
+ this.$submitBtnLoader = this.$submitBtn.find('.js-btn-spinner');
+ this.$submitBtnLabel = this.$submitBtn.find('.js-btn-label');
+ }
+
+ init() {
+ // Initialize View
+ this.toggleServiceState(this.$serviceToggle.is(':checked'));
+
+ // Bind Event Listeners
+ this.$serviceToggle.on('change', e => this.handleServiceToggle(e));
+ this.$submitBtn.on('click', e => this.handleSettingsSave(e));
+ }
+
+ handleSettingsSave(e) {
+ // Check if Service is marked active, as if not marked active,
+ // We can skip testing it and directly go ahead to allow form to
+ // be submitted
+ if (!this.$serviceToggle.is(':checked')) {
+ return;
+ }
+
+ // Service was marked active so now we check;
+ // 1) If form contents are valid
+ // 2) If this service can be tested
+ // If both conditions are true, we override form submission
+ // and test the service using provided configuration.
+ if (this.$form.get(0).checkValidity() && this.canTestService) {
+ e.preventDefault();
+ this.testSettings(this.$form.serialize());
+ }
+ }
+
+ handleServiceToggle(e) {
+ this.toggleServiceState($(e.currentTarget).is(':checked'));
+ }
+
+ /**
+ * Change Form's validation enforcement based on service status (active/inactive)
+ */
+ toggleServiceState(serviceActive) {
+ this.toggleSubmitBtnLabel(serviceActive);
+ if (serviceActive) {
+ this.$form.removeAttr('novalidate');
+ } else if (!this.$form.attr('novalidate')) {
+ this.$form.attr('novalidate', 'novalidate');
+ }
+ }
+
+ /**
+ * Toggle Submit button label based on Integration status and ability to test service
+ */
+ toggleSubmitBtnLabel(serviceActive) {
+ let btnLabel = 'Save changes';
+
+ if (serviceActive && this.canTestService) {
+ btnLabel = 'Test settings and save changes';
+ }
+
+ this.$submitBtnLabel.text(btnLabel);
+ }
+
+ /**
+ * Toggle Submit button state based on provided boolean value of `saveTestActive`
+ * When enabled, it does two things, and reverts back when disabled
+ *
+ * 1. It shows load spinner on submit button
+ * 2. Makes submit button disabled
+ */
+ toggleSubmitBtnState(saveTestActive) {
+ if (saveTestActive) {
+ this.$submitBtn.disable();
+ this.$submitBtnLoader.removeClass('hidden');
+ } else {
+ this.$submitBtn.enable();
+ this.$submitBtnLoader.addClass('hidden');
+ }
+ }
+
+ /* eslint-disable promise/catch-or-return, no-new */
+ /**
+ * Test Integration config
+ */
+ testSettings(formData) {
+ this.toggleSubmitBtnState(true);
+ $.ajax({
+ type: 'PUT',
+ url: this.testEndPoint,
+ data: formData,
+ })
+ .done((res) => {
+ if (res.error) {
+ new Flash(res.message, null, null, {
+ title: 'Save anyway',
+ clickHandler: (e) => {
+ e.preventDefault();
+ this.$form.submit();
+ },
+ });
+ } else {
+ this.$form.submit();
+ }
+ })
+ .fail(() => {
+ new Flash('Something went wrong on our end.');
+ })
+ .always(() => {
+ this.toggleSubmitBtnState(false);
+ });
+ }
+}
diff --git a/app/assets/javascripts/lib/utils/url_utility.js b/app/assets/javascripts/lib/utils/url_utility.js
index b9d2fc25c39..3328ff9cc23 100644
--- a/app/assets/javascripts/lib/utils/url_utility.js
+++ b/app/assets/javascripts/lib/utils/url_utility.js
@@ -66,7 +66,8 @@ w.gl.utils.removeParamQueryString = function(url, param) {
})()).join('&');
};
w.gl.utils.removeParams = (params) => {
- const url = new URL(window.location.href);
+ const url = document.createElement('a');
+ url.href = window.location.href;
params.forEach((param) => {
url.search = w.gl.utils.removeParamQueryString(url.search, param);
});
diff --git a/app/assets/javascripts/notebook/cells/markdown.vue b/app/assets/javascripts/notebook/cells/markdown.vue
index 3e8240d10ec..814d2ea92b4 100644
--- a/app/assets/javascripts/notebook/cells/markdown.vue
+++ b/app/assets/javascripts/notebook/cells/markdown.vue
@@ -30,7 +30,7 @@
|
\\s\\$(?!\\$)
)
- (.+?)
+ ((.|\\n)+?)
(
\\s\\\\end{[a-zA-Z]+}$
|
@@ -45,15 +45,25 @@
let inline = false;
if (typeof katex !== 'undefined') {
- const katexString = text.replace(/\\/g, '\\');
- const matches = new RegExp(katexRegexString, 'gi').exec(katexString);
+ const katexString = text.replace(/&amp;/g, '&')
+ .replace(/&=&/g, '\\space=\\space')
+ .replace(/<(\/?)em>/g, '_');
+ const regex = new RegExp(katexRegexString, 'gi');
+ const matchLocation = katexString.search(regex);
+ const numberOfMatches = katexString.match(regex);
- if (matches && matches.length > 0) {
- if (matches[1].trim() === '$' && matches[3].trim() === '$') {
+ if (numberOfMatches && numberOfMatches.length !== 0) {
+ if (matchLocation > 0) {
+ let matches = regex.exec(katexString);
inline = true;
- text = `${katexString.replace(matches[0], '')} ${katex.renderToString(matches[2])}`;
+ while (matches !== null) {
+ const renderedKatex = katex.renderToString(matches[0].replace(/\$/g, ''));
+ text = `${text.replace(matches[0], ` ${renderedKatex}`)}`;
+ matches = regex.exec(katexString);
+ }
} else {
+ const matches = regex.exec(katexString);
text = katex.renderToString(matches[2]);
}
}
@@ -79,7 +89,7 @@
},
computed: {
markdown() {
- return marked(this.cell.source.join(''));
+ return marked(this.cell.source.join('').replace(/\\/g, '\\\\'));
},
},
};
diff --git a/app/assets/javascripts/pipelines/components/header_component.vue b/app/assets/javascripts/pipelines/components/header_component.vue
new file mode 100644
index 00000000000..4f6c5c177cf
--- /dev/null
+++ b/app/assets/javascripts/pipelines/components/header_component.vue
@@ -0,0 +1,97 @@
+<script>
+import ciHeader from '../../vue_shared/components/header_ci_component.vue';
+import eventHub from '../event_hub';
+import loadingIcon from '../../vue_shared/components/loading_icon.vue';
+
+export default {
+ name: 'PipelineHeaderSection',
+ props: {
+ pipeline: {
+ type: Object,
+ required: true,
+ },
+ isLoading: {
+ type: Boolean,
+ required: true,
+ },
+ },
+ components: {
+ ciHeader,
+ loadingIcon,
+ },
+
+ data() {
+ return {
+ actions: this.getActions(),
+ };
+ },
+
+ computed: {
+ status() {
+ return this.pipeline.details && this.pipeline.details.status;
+ },
+ shouldRenderContent() {
+ return !this.isLoading && Object.keys(this.pipeline).length;
+ },
+ },
+
+ methods: {
+ postAction(action) {
+ const index = this.actions.indexOf(action);
+
+ this.$set(this.actions[index], 'isLoading', true);
+
+ eventHub.$emit('headerPostAction', action);
+ },
+
+ getActions() {
+ const actions = [];
+
+ if (this.pipeline.retry_path) {
+ actions.push({
+ label: 'Retry',
+ path: this.pipeline.retry_path,
+ cssClass: 'js-retry-button btn btn-inverted-secondary',
+ type: 'button',
+ isLoading: false,
+ });
+ }
+
+ if (this.pipeline.cancel_path) {
+ actions.push({
+ label: 'Cancel running',
+ path: this.pipeline.cancel_path,
+ cssClass: 'js-btn-cancel-pipeline btn btn-danger',
+ type: 'button',
+ isLoading: false,
+ });
+ }
+
+ return actions;
+ },
+ },
+
+ watch: {
+ pipeline() {
+ this.actions = this.getActions();
+ },
+ },
+};
+</script>
+<template>
+ <div class="pipeline-header-container">
+ <ci-header
+ v-if="shouldRenderContent"
+ :status="status"
+ item-name="Pipeline"
+ :item-id="pipeline.id"
+ :time="pipeline.created_at"
+ :user="pipeline.user"
+ :actions="actions"
+ @actionClicked="postAction"
+ />
+ <loading-icon
+ v-else
+ size="2"/>
+ </div>
+</template>
diff --git a/app/assets/javascripts/pipelines/components/pipeline_url.vue b/app/assets/javascripts/pipelines/components/pipeline_url.vue
index b8457fae967..4781a8ff1da 100644
--- a/app/assets/javascripts/pipelines/components/pipeline_url.vue
+++ b/app/assets/javascripts/pipelines/components/pipeline_url.vue
@@ -33,7 +33,7 @@ export default {
<user-avatar-link
v-if="user"
class="js-pipeline-url-user"
- :link-href="pipeline.user.web_url"
+ :link-href="pipeline.user.path"
:img-src="pipeline.user.avatar_url"
:tooltip-text="pipeline.user.name"
/>
diff --git a/app/assets/javascripts/pipelines/pipeline_details_bundle.js b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
index 5aab25e0348..bfc416da50b 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_bundle.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_bundle.js
@@ -1,6 +1,10 @@
+/* global Flash */
+
import Vue from 'vue';
import PipelinesMediator from './pipeline_details_mediatior';
import pipelineGraph from './components/graph/graph_component.vue';
+import pipelineHeader from './components/header_component.vue';
+import eventHub from './event_hub';
document.addEventListener('DOMContentLoaded', () => {
const dataset = document.querySelector('.js-pipeline-details-vue').dataset;
@@ -9,7 +13,8 @@ document.addEventListener('DOMContentLoaded', () => {
mediator.fetchPipeline();
- const pipelineGraphApp = new Vue({
+ // eslint-disable-next-line
+ new Vue({
el: '#js-pipeline-graph-vue',
data() {
return {
@@ -29,5 +34,37 @@ document.addEventListener('DOMContentLoaded', () => {
},
});
- return pipelineGraphApp;
+ // eslint-disable-next-line
+ new Vue({
+ el: '#js-pipeline-header-vue',
+ data() {
+ return {
+ mediator,
+ };
+ },
+ components: {
+ pipelineHeader,
+ },
+ created() {
+ eventHub.$on('headerPostAction', this.postAction);
+ },
+ beforeDestroy() {
+ eventHub.$off('headerPostAction', this.postAction);
+ },
+ methods: {
+ postAction(action) {
+ this.mediator.service.postAction(action.path)
+ .then(() => this.mediator.refreshPipeline())
+ .catch(() => new Flash('An error occurred while making the request.'));
+ },
+ },
+ render(createElement) {
+ return createElement('pipeline-header', {
+ props: {
+ isLoading: this.mediator.state.isLoading,
+ pipeline: this.mediator.store.state.pipeline,
+ },
+ });
+ },
+ });
});
diff --git a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
index b9a6d5ca5fc..82537ea06f5 100644
--- a/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
+++ b/app/assets/javascripts/pipelines/pipeline_details_mediatior.js
@@ -26,6 +26,8 @@ export default class pipelinesMediator {
if (!Visibility.hidden()) {
this.state.isLoading = true;
this.poll.makeRequest();
+ } else {
+ this.refreshPipeline();
}
Visibility.change(() => {
@@ -48,4 +50,10 @@ export default class pipelinesMediator {
this.state.isLoading = false;
return new Flash('An error occurred while fetching the pipeline.');
}
+
+ refreshPipeline() {
+ this.service.getPipeline()
+ .then(response => this.successCallback(response))
+ .catch(() => this.errorCallback());
+ }
}
diff --git a/app/assets/javascripts/pipelines/pipelines.js b/app/assets/javascripts/pipelines/pipelines.js
index d6952d1ee5f..9f247af1dec 100644
--- a/app/assets/javascripts/pipelines/pipelines.js
+++ b/app/assets/javascripts/pipelines/pipelines.js
@@ -169,7 +169,7 @@ export default {
eventHub.$on('refreshPipelines', this.fetchPipelines);
},
- beforeDestroyed() {
+ beforeDestroy() {
eventHub.$off('refreshPipelines');
},
diff --git a/app/assets/javascripts/pipelines/services/pipeline_service.js b/app/assets/javascripts/pipelines/services/pipeline_service.js
index f1cc60c1ee0..3e0c52c7726 100644
--- a/app/assets/javascripts/pipelines/services/pipeline_service.js
+++ b/app/assets/javascripts/pipelines/services/pipeline_service.js
@@ -11,4 +11,9 @@ export default class PipelineService {
getPipeline() {
return this.pipeline.get();
}
+
+ // eslint-disable-next-line
+ postAction(endpoint) {
+ return Vue.http.post(`${endpoint}.json`);
+ }
}
diff --git a/app/assets/javascripts/pipelines/services/pipelines_service.js b/app/assets/javascripts/pipelines/services/pipelines_service.js
index b21f84b4545..e2285494e62 100644
--- a/app/assets/javascripts/pipelines/services/pipelines_service.js
+++ b/app/assets/javascripts/pipelines/services/pipelines_service.js
@@ -33,8 +33,6 @@ export default class PipelinesService {
/**
* Post request for all pipelines actions.
- * Endpoint content type needs to be:
- * `Content-Type:application/x-www-form-urlencoded`
*
* @param {String} endpoint
* @return {Promise}
diff --git a/app/assets/javascripts/vue_shared/components/commit.js b/app/assets/javascripts/vue_shared/components/commit.js
index 23bc5fbc034..8e22057e2e9 100644
--- a/app/assets/javascripts/vue_shared/components/commit.js
+++ b/app/assets/javascripts/vue_shared/components/commit.js
@@ -91,7 +91,7 @@ export default {
hasAuthor() {
return this.author &&
this.author.avatar_url &&
- this.author.web_url &&
+ this.author.path &&
this.author.username;
},
@@ -140,7 +140,7 @@ export default {
<user-avatar-link
v-if="hasAuthor"
class="avatar-image-container"
- :link-href="author.web_url"
+ :link-href="author.path"
:img-src="author.avatar_url"
:img-alt="userImageAltDescription"
:tooltip-text="author.username"
diff --git a/app/assets/javascripts/vue_shared/components/header_ci_component.vue b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
index fd0dcd716d6..fe6d6a792e7 100644
--- a/app/assets/javascripts/vue_shared/components/header_ci_component.vue
+++ b/app/assets/javascripts/vue_shared/components/header_ci_component.vue
@@ -1,8 +1,9 @@
<script>
import ciIconBadge from './ci_badge_link.vue';
+import loadingIcon from './loading_icon.vue';
import timeagoTooltip from './time_ago_tooltip.vue';
import tooltipMixin from '../mixins/tooltip';
-import userAvatarLink from './user_avatar/user_avatar_link.vue';
+import userAvatarImage from './user_avatar/user_avatar_image.vue';
/**
* Renders header component for job and pipeline page based on UI mockups
@@ -31,7 +32,8 @@ export default {
},
user: {
type: Object,
- required: true,
+ required: false,
+ default: () => ({}),
},
actions: {
type: Array,
@@ -46,8 +48,9 @@ export default {
components: {
ciIconBadge,
+ loadingIcon,
timeagoTooltip,
- userAvatarLink,
+ userAvatarImage,
},
computed: {
@@ -58,13 +61,13 @@ export default {
methods: {
onClickAction(action) {
- this.$emit('postAction', action);
+ this.$emit('actionClicked', action);
},
},
};
</script>
<template>
- <header class="page-content-header top-area">
+ <header class="page-content-header">
<section class="header-main-content">
<ci-icon-badge :status="status" />
@@ -79,21 +82,23 @@ export default {
by
- <user-avatar-link
- :link-href="user.web_url"
- :img-src="user.avatar_url"
- :img-alt="userAvatarAltText"
- :tooltip-text="user.name"
- :img-size="24"
- />
-
- <a
- :href="user.web_url"
- :title="user.email"
- class="js-user-link commit-committer-link"
- ref="tooltip">
- {{user.name}}
- </a>
+ <template v-if="user">
+ <a
+ :href="user.path"
+ :title="user.email"
+ class="js-user-link commit-committer-link"
+ ref="tooltip">
+
+ <user-avatar-image
+ :img-src="user.avatar_url"
+ :img-alt="userAvatarAltText"
+ :tooltip-text="user.name"
+ :img-size="24"
+ />
+
+ {{user.name}}
+ </a>
+ </template>
</section>
<section
@@ -111,11 +116,17 @@ export default {
<button
v-else="action.type === 'button'"
@click="onClickAction(action)"
+ :disabled="action.isLoading"
:class="action.cssClass"
type="button">
{{action.label}}
- </button>
+ <i
+ v-show="action.isLoading"
+ class="fa fa-spin fa-spinner"
+ aria-hidden="true">
+ </i>
+ </button>
</template>
</section>
</header>
diff --git a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
index 3283a6bcacc..f60f8eeb43d 100644
--- a/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
+++ b/app/assets/javascripts/vue_shared/components/pipelines_table_row.js
@@ -83,7 +83,7 @@ export default {
} else {
commitAuthorInformation = {
avatar_url: this.pipeline.commit.author_gravatar_url,
- web_url: `mailto:${this.pipeline.commit.author_email}`,
+ path: `mailto:${this.pipeline.commit.author_email}`,
username: this.pipeline.commit.author_name,
};
}
diff --git a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
index b8db6afda12..cd6f8c7aee4 100644
--- a/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
+++ b/app/assets/javascripts/vue_shared/components/user_avatar/user_avatar_image.vue
@@ -60,6 +60,12 @@ export default {
avatarSizeClass() {
return `s${this.size}`;
},
+ // API response sends null when gravatar is disabled and
+ // we provide an empty string when we use it inside user avatar link.
+ // In both cases we should render the defaultAvatarUrl
+ imageSource() {
+ return this.imgSrc === '' || this.imgSrc === null ? defaultAvatarUrl : this.imgSrc;
+ },
},
};
</script>
@@ -68,7 +74,7 @@ export default {
<img
class="avatar"
:class="[avatarSizeClass, cssClasses]"
- :src="imgSrc"
+ :src="imageSource"
:width="size"
:height="size"
:alt="imgAlt"
diff --git a/app/assets/stylesheets/framework/files.scss b/app/assets/stylesheets/framework/files.scss
index 78f425057eb..d08df05fd6c 100644
--- a/app/assets/stylesheets/framework/files.scss
+++ b/app/assets/stylesheets/framework/files.scss
@@ -85,7 +85,7 @@
}
/**
- * Blame file
+ * Annotate file
*/
&.blame {
table {
diff --git a/app/assets/stylesheets/framework/filters.scss b/app/assets/stylesheets/framework/filters.scss
index 90051ffe753..585f4871f5f 100644
--- a/app/assets/stylesheets/framework/filters.scss
+++ b/app/assets/stylesheets/framework/filters.scss
@@ -90,6 +90,7 @@
.filtered-search-term {
display: -webkit-flex;
display: flex;
+ flex-shrink: 0;
margin-top: 5px;
margin-bottom: 5px;
@@ -239,7 +240,7 @@
width: 35px;
background-color: $white-light;
border: none;
- position: absolute;
+ position: static;
right: 0;
height: 100%;
outline: none;
diff --git a/app/assets/stylesheets/framework/flash.scss b/app/assets/stylesheets/framework/flash.scss
index 25b4feca3c3..38d884bc7eb 100644
--- a/app/assets/stylesheets/framework/flash.scss
+++ b/app/assets/stylesheets/framework/flash.scss
@@ -16,6 +16,22 @@
@extend .alert;
@extend .alert-danger;
margin: 0;
+
+ .flash-text,
+ .flash-action {
+ display: inline-block;
+ }
+
+ a.flash-action {
+ margin-left: 5px;
+ text-decoration: none;
+ font-weight: normal;
+ border-bottom: 1px solid;
+
+ &:hover {
+ border-color: transparent;
+ }
+ }
}
.flash-notice,
diff --git a/app/assets/stylesheets/framework/timeline.scss b/app/assets/stylesheets/framework/timeline.scss
index cec3b54d567..10881987038 100644
--- a/app/assets/stylesheets/framework/timeline.scss
+++ b/app/assets/stylesheets/framework/timeline.scss
@@ -4,7 +4,7 @@
padding: 0;
&::before {
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', $screen-xs-min) {
background: none;
}
}
@@ -30,7 +30,7 @@
.timeline-entry-inner {
position: relative;
- @include notes-media('max', $screen-xs-max) {
+ @include notes-media('max', $screen-xs-min) {
.timeline-icon {
display: none;
}
diff --git a/app/assets/stylesheets/pages/pipelines.scss b/app/assets/stylesheets/pages/pipelines.scss
index cf2e565dd2d..58b458cd837 100644
--- a/app/assets/stylesheets/pages/pipelines.scss
+++ b/app/assets/stylesheets/pages/pipelines.scss
@@ -984,3 +984,11 @@
width: 12px;
}
}
+
+.pipeline-header-container {
+ min-height: 55px;
+
+ .text-center {
+ padding-top: 12px;
+ }
+}
diff --git a/app/controllers/admin/keys_controller.rb b/app/controllers/admin/keys_controller.rb
index 054bb52b696..299419fb509 100644
--- a/app/controllers/admin/keys_controller.rb
+++ b/app/controllers/admin/keys_controller.rb
@@ -15,9 +15,9 @@ class Admin::KeysController < Admin::ApplicationController
respond_to do |format|
if key.destroy
- format.html { redirect_to [:admin, user], notice: 'User key was successfully removed.' }
+ format.html { redirect_to keys_admin_user_path(user), notice: 'User key was successfully removed.' }
else
- format.html { redirect_to [:admin, user], alert: 'Failed to remove user key.' }
+ format.html { redirect_to keys_admin_user_path(user), alert: 'Failed to remove user key.' }
end
end
end
diff --git a/app/controllers/concerns/renders_blob.rb b/app/controllers/concerns/renders_blob.rb
index 1d37e4cb3bd..54dcd7c61ce 100644
--- a/app/controllers/concerns/renders_blob.rb
+++ b/app/controllers/concerns/renders_blob.rb
@@ -18,7 +18,7 @@ module RendersBlob
}
end
- def override_max_blob_size(blob)
- blob.override_max_size! if params[:override_max_size] == 'true'
+ def conditionally_expand_blob(blob)
+ blob.expand! if params[:expanded] == 'true'
end
end
diff --git a/app/controllers/projects/artifacts_controller.rb b/app/controllers/projects/artifacts_controller.rb
index b46a33604ff..ea036b1f705 100644
--- a/app/controllers/projects/artifacts_controller.rb
+++ b/app/controllers/projects/artifacts_controller.rb
@@ -27,7 +27,7 @@ class Projects::ArtifactsController < Projects::ApplicationController
def file
blob = @entry.blob
- override_max_blob_size(blob)
+ conditionally_expand_blob(blob)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/blob_controller.rb b/app/controllers/projects/blob_controller.rb
index 87721fbe2f5..7025c7a1de6 100644
--- a/app/controllers/projects/blob_controller.rb
+++ b/app/controllers/projects/blob_controller.rb
@@ -35,7 +35,7 @@ class Projects::BlobController < Projects::ApplicationController
end
def show
- override_max_blob_size(@blob)
+ conditionally_expand_blob(@blob)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/environments_controller.rb b/app/controllers/projects/environments_controller.rb
index efe83776834..4630f451445 100644
--- a/app/controllers/projects/environments_controller.rb
+++ b/app/controllers/projects/environments_controller.rb
@@ -15,6 +15,8 @@ class Projects::EnvironmentsController < Projects::ApplicationController
respond_to do |format|
format.html
format.json do
+ Gitlab::PollingInterval.set_header(response, interval: 3_000)
+
render json: {
environments: EnvironmentSerializer
.new(project: @project, current_user: @current_user)
diff --git a/app/controllers/projects/services_controller.rb b/app/controllers/projects/services_controller.rb
index f9d798d0455..704f8cc8a79 100644
--- a/app/controllers/projects/services_controller.rb
+++ b/app/controllers/projects/services_controller.rb
@@ -4,6 +4,7 @@ class Projects::ServicesController < Projects::ApplicationController
# Authorize
before_action :authorize_admin_project!
before_action :service, only: [:edit, :update, :test]
+ before_action :update_service, only: [:update, :test]
respond_to :html
@@ -13,36 +14,46 @@ class Projects::ServicesController < Projects::ApplicationController
end
def update
- @service.assign_attributes(service_params[:service])
if @service.save(context: :manual_change)
- redirect_to(
- edit_namespace_project_service_path(@project.namespace, @project, @service.to_param),
- notice: 'Successfully updated.'
- )
+ redirect_to(namespace_project_settings_integrations_path(@project.namespace, @project), notice: success_message)
else
render 'edit'
end
end
def test
- return render_404 unless @service.can_test?
+ message = {}
+
+ if @service.can_test?
+ data = @service.test_data(project, current_user)
+ outcome = @service.test(data)
- data = @service.test_data(project, current_user)
- outcome = @service.test(data)
+ unless outcome[:success]
+ message = { error: true, message: 'Test failed.', service_response: outcome[:result].to_s }
+ end
- if outcome[:success]
- message = { notice: 'We sent a request to the provided URL' }
+ status = :ok
else
- error_message = "We tried to send a request to the provided URL but an error occurred"
- error_message << ": #{outcome[:result]}" if outcome[:result].present?
- message = { alert: error_message }
+ status = :not_found
end
- redirect_back_or_default(options: message)
+ render json: message, status: status
end
private
+ def success_message
+ if @service.active?
+ "#{@service.title} activated."
+ else
+ "#{@service.title} settings saved, but not activated."
+ end
+ end
+
+ def update_service
+ @service.assign_attributes(service_params[:service])
+ end
+
def service
@service ||= @project.find_or_initialize_service(params[:id])
end
diff --git a/app/controllers/projects/snippets_controller.rb b/app/controllers/projects/snippets_controller.rb
index 3b2b0d9e502..3a97c1e98af 100644
--- a/app/controllers/projects/snippets_controller.rb
+++ b/app/controllers/projects/snippets_controller.rb
@@ -56,7 +56,7 @@ class Projects::SnippetsController < Projects::ApplicationController
def show
blob = @snippet.blob
- override_max_blob_size(blob)
+ conditionally_expand_blob(blob)
respond_to do |format|
format.html do
diff --git a/app/controllers/projects/variables_controller.rb b/app/controllers/projects/variables_controller.rb
index a4d1b1ee69b..0953eecaeb5 100644
--- a/app/controllers/projects/variables_controller.rb
+++ b/app/controllers/projects/variables_controller.rb
@@ -42,6 +42,7 @@ class Projects::VariablesController < Projects::ApplicationController
private
def project_params
- params.require(:variable).permit([:id, :key, :value, :_destroy])
+ params.require(:variable)
+ .permit([:id, :key, :value, :protected, :_destroy])
end
end
diff --git a/app/controllers/snippets_controller.rb b/app/controllers/snippets_controller.rb
index 7445f61195d..5b2d143ee79 100644
--- a/app/controllers/snippets_controller.rb
+++ b/app/controllers/snippets_controller.rb
@@ -58,7 +58,7 @@ class SnippetsController < ApplicationController
def show
blob = @snippet.blob
- override_max_blob_size(blob)
+ conditionally_expand_blob(blob)
@note = Note.new(noteable: @snippet)
@noteable = @snippet
diff --git a/app/helpers/avatars_helper.rb b/app/helpers/avatars_helper.rb
index b7e0ff8ecd0..bbe7f3c8fb4 100644
--- a/app/helpers/avatars_helper.rb
+++ b/app/helpers/avatars_helper.rb
@@ -8,18 +8,28 @@ module AvatarsHelper
}))
end
- def user_avatar(options = {})
+ def user_avatar_without_link(options = {})
avatar_size = options[:size] || 16
user_name = options[:user].try(:name) || options[:user_name]
css_class = options[:css_class] || ''
-
- avatar = image_tag(
- avatar_icon(options[:user] || options[:user_email], avatar_size),
+ avatar_url = options[:url] || avatar_icon(options[:user] || options[:user_email], avatar_size)
+ data_attributes = { container: 'body' }
+
+ if options[:lazy]
+ data_attributes[:src] = avatar_url
+ end
+
+ image_tag(
+ options[:lazy] ? '' : avatar_url,
class: "avatar has-tooltip s#{avatar_size} #{css_class}",
alt: "#{user_name}'s avatar",
title: user_name,
- data: { container: 'body' }
+ data: data_attributes
)
+ end
+
+ def user_avatar(options = {})
+ avatar = user_avatar_without_link(options)
if options[:user]
link_to(avatar, user_path(options[:user]))
diff --git a/app/helpers/blob_helper.rb b/app/helpers/blob_helper.rb
index 11c972c6563..3efa7c36057 100644
--- a/app/helpers/blob_helper.rb
+++ b/app/helpers/blob_helper.rb
@@ -240,14 +240,10 @@ module BlobHelper
def blob_render_error_reason(viewer)
case viewer.render_error
+ when :collapsed
+ "it is larger than #{number_to_human_size(viewer.collapse_limit)}"
when :too_large
- max_size =
- if viewer.can_override_max_size?
- viewer.overridable_max_size
- else
- viewer.max_size
- end
- "it is larger than #{number_to_human_size(max_size)}"
+ "it is larger than #{number_to_human_size(viewer.size_limit)}"
when :server_side_but_stored_externally
case viewer.blob.external_storage
when :lfs
@@ -264,8 +260,8 @@ module BlobHelper
error = viewer.render_error
options = []
- if error == :too_large && viewer.can_override_max_size?
- options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, override_max_size: true, format: nil)))
+ if error == :collapsed
+ options << link_to('load it anyway', url_for(params.merge(viewer: viewer.type, expanded: true, format: nil)))
end
# If the error is `:server_side_but_stored_externally`, the simple viewer will show the same error,
diff --git a/app/helpers/diff_helper.rb b/app/helpers/diff_helper.rb
index 4c4fbdd4d39..2ae3a616933 100644
--- a/app/helpers/diff_helper.rb
+++ b/app/helpers/diff_helper.rb
@@ -8,8 +8,8 @@ module DiffHelper
[marked_old_line, marked_new_line]
end
- def expand_all_diffs?
- params[:expand_all_diffs].present?
+ def diffs_expanded?
+ params[:expanded].present?
end
def diff_view
@@ -22,10 +22,10 @@ module DiffHelper
end
def diff_options
- options = { ignore_whitespace_change: hide_whitespace?, no_collapse: expand_all_diffs? }
+ options = { ignore_whitespace_change: hide_whitespace?, expanded: diffs_expanded? }
if action_name == 'diff_for_path'
- options[:no_collapse] = true
+ options[:expanded] = true
options[:paths] = params.values_at(:old_path, :new_path)
end
@@ -66,12 +66,12 @@ module DiffHelper
discussions_left = discussions_right = nil
- if left && (left.unchanged? || left.removed?)
+ if left && (left.unchanged? || left.discussable?)
line_code = diff_file.line_code(left)
discussions_left = @grouped_diff_discussions[line_code]
end
- if right && right.added?
+ if right&.discussable?
line_code = diff_file.line_code(right)
discussions_right = @grouped_diff_discussions[line_code]
end
diff --git a/app/helpers/notes_helper.rb b/app/helpers/notes_helper.rb
index 375110b77e2..3d4802290b5 100644
--- a/app/helpers/notes_helper.rb
+++ b/app/helpers/notes_helper.rb
@@ -50,7 +50,7 @@ module NotesHelper
def link_to_reply_discussion(discussion, line_type = nil)
return unless current_user
- data = { discussion_id: discussion.id, line_type: line_type }
+ data = { discussion_id: discussion.reply_id, line_type: line_type }
button_tag 'Reply...', class: 'btn btn-text-field js-discussion-reply-button',
data: data, title: 'Add a reply'
diff --git a/app/models/application_setting.rb b/app/models/application_setting.rb
index 043f57241a3..3d12f3c306b 100644
--- a/app/models/application_setting.rb
+++ b/app/models/application_setting.rb
@@ -13,13 +13,13 @@ class ApplicationSetting < ActiveRecord::Base
[\r\n] # any number of newline characters
}x
- serialize :restricted_visibility_levels
- serialize :import_sources
- serialize :disabled_oauth_sign_in_sources, Array
- serialize :domain_whitelist, Array
- serialize :domain_blacklist, Array
- serialize :repository_storages
- serialize :sidekiq_throttling_queues, Array
+ serialize :restricted_visibility_levels # rubocop:disable Cop/ActiverecordSerialize
+ serialize :import_sources # rubocop:disable Cop/ActiverecordSerialize
+ serialize :disabled_oauth_sign_in_sources, Array # rubocop:disable Cop/ActiverecordSerialize
+ serialize :domain_whitelist, Array # rubocop:disable Cop/ActiverecordSerialize
+ serialize :domain_blacklist, Array # rubocop:disable Cop/ActiverecordSerialize
+ serialize :repository_storages # rubocop:disable Cop/ActiverecordSerialize
+ serialize :sidekiq_throttling_queues, Array # rubocop:disable Cop/ActiverecordSerialize
cache_markdown_field :sign_in_text
cache_markdown_field :help_page_text
diff --git a/app/models/audit_event.rb b/app/models/audit_event.rb
index 967ffd46db0..46d412fbd72 100644
--- a/app/models/audit_event.rb
+++ b/app/models/audit_event.rb
@@ -1,5 +1,5 @@
class AuditEvent < ActiveRecord::Base
- serialize :details, Hash
+ serialize :details, Hash # rubocop:disable Cop/ActiverecordSerialize
belongs_to :user, foreign_key: :author_id
diff --git a/app/models/blob.rb b/app/models/blob.rb
index e75926241ba..6a42a12891c 100644
--- a/app/models/blob.rb
+++ b/app/models/blob.rb
@@ -102,10 +102,6 @@ class Blob < SimpleDelegator
raw_size == 0
end
- def too_large?
- size && truncated?
- end
-
def external_storage_error?
if external_storage == :lfs
!project&.lfs_enabled?
@@ -160,7 +156,7 @@ class Blob < SimpleDelegator
end
def readable_text?
- text? && !stored_externally? && !too_large?
+ text? && !stored_externally? && !truncated?
end
def simple_viewer
@@ -187,9 +183,9 @@ class Blob < SimpleDelegator
rendered_as_text? && rich_viewer
end
- def override_max_size!
- simple_viewer&.override_max_size = true
- rich_viewer&.override_max_size = true
+ def expand!
+ simple_viewer&.expanded = true
+ rich_viewer&.expanded = true
end
private
diff --git a/app/models/blob_viewer/auxiliary.rb b/app/models/blob_viewer/auxiliary.rb
index 07a207730cf..1bea225f17c 100644
--- a/app/models/blob_viewer/auxiliary.rb
+++ b/app/models/blob_viewer/auxiliary.rb
@@ -7,8 +7,8 @@ module BlobViewer
included do
self.loading_partial_name = 'loading_auxiliary'
self.type = :auxiliary
- self.overridable_max_size = 100.kilobytes
- self.max_size = 100.kilobytes
+ self.collapse_limit = 100.kilobytes
+ self.size_limit = 100.kilobytes
end
def visible_to?(current_user)
diff --git a/app/models/blob_viewer/base.rb b/app/models/blob_viewer/base.rb
index 26a3778c2a3..e6119d25fab 100644
--- a/app/models/blob_viewer/base.rb
+++ b/app/models/blob_viewer/base.rb
@@ -2,14 +2,14 @@ module BlobViewer
class Base
PARTIAL_PATH_PREFIX = 'projects/blob/viewers'.freeze
- class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_types, :load_async, :binary, :switcher_icon, :switcher_title, :overridable_max_size, :max_size
+ class_attribute :partial_name, :loading_partial_name, :type, :extensions, :file_types, :load_async, :binary, :switcher_icon, :switcher_title, :collapse_limit, :size_limit
self.loading_partial_name = 'loading'
delegate :partial_path, :loading_partial_path, :rich?, :simple?, :text?, :binary?, to: :class
attr_reader :blob
- attr_accessor :override_max_size
+ attr_accessor :expanded
delegate :project, to: :blob
@@ -61,24 +61,16 @@ module BlobViewer
self.class.load_async? && render_error.nil?
end
- def exceeds_overridable_max_size?
- overridable_max_size && blob.raw_size > overridable_max_size
- end
-
- def exceeds_max_size?
- max_size && blob.raw_size > max_size
- end
+ def collapsed?
+ return @collapsed if defined?(@collapsed)
- def can_override_max_size?
- exceeds_overridable_max_size? && !exceeds_max_size?
+ @collapsed = !expanded && collapse_limit && blob.raw_size > collapse_limit
end
def too_large?
- if override_max_size
- exceeds_max_size?
- else
- exceeds_overridable_max_size?
- end
+ return @too_large if defined?(@too_large)
+
+ @too_large = size_limit && blob.raw_size > size_limit
end
# This method is used on the server side to check whether we can attempt to
@@ -95,6 +87,8 @@ module BlobViewer
def render_error
if too_large?
:too_large
+ elsif collapsed?
+ :collapsed
end
end
diff --git a/app/models/blob_viewer/client_side.rb b/app/models/blob_viewer/client_side.rb
index cc68236f92b..079cfbe3616 100644
--- a/app/models/blob_viewer/client_side.rb
+++ b/app/models/blob_viewer/client_side.rb
@@ -4,8 +4,8 @@ module BlobViewer
included do
self.load_async = false
- self.overridable_max_size = 10.megabytes
- self.max_size = 50.megabytes
+ self.collapse_limit = 10.megabytes
+ self.size_limit = 50.megabytes
end
end
end
diff --git a/app/models/blob_viewer/server_side.rb b/app/models/blob_viewer/server_side.rb
index 87884dcd6bf..05a3dd7d913 100644
--- a/app/models/blob_viewer/server_side.rb
+++ b/app/models/blob_viewer/server_side.rb
@@ -4,8 +4,8 @@ module BlobViewer
included do
self.load_async = true
- self.overridable_max_size = 2.megabytes
- self.max_size = 5.megabytes
+ self.collapse_limit = 2.megabytes
+ self.size_limit = 5.megabytes
end
def prepare!
diff --git a/app/models/blob_viewer/text.rb b/app/models/blob_viewer/text.rb
index eddca50b4d4..f68cbb7e212 100644
--- a/app/models/blob_viewer/text.rb
+++ b/app/models/blob_viewer/text.rb
@@ -5,7 +5,7 @@ module BlobViewer
self.partial_name = 'text'
self.binary = false
- self.overridable_max_size = 1.megabyte
- self.max_size = 10.megabytes
+ self.collapse_limit = 1.megabyte
+ self.size_limit = 10.megabytes
end
end
diff --git a/app/models/ci/build.rb b/app/models/ci/build.rb
index 2d49320a631..fefb02740a2 100644
--- a/app/models/ci/build.rb
+++ b/app/models/ci/build.rb
@@ -19,8 +19,8 @@ module Ci
)
end
- serialize :options
- serialize :yaml_variables, Gitlab::Serializer::Ci::Variables
+ serialize :options # rubocop:disable Cop/ActiverecordSerialize
+ serialize :yaml_variables, Gitlab::Serializer::Ci::Variables # rubocop:disable Cop/ActiverecordSerialize
delegate :name, to: :project, prefix: true
@@ -191,7 +191,7 @@ module Ci
variables += project.deployment_variables if has_environment?
variables += yaml_variables
variables += user_variables
- variables += project.secret_variables
+ variables += project.secret_variables_for(ref).map(&:to_runner_variable)
variables += trigger_request.user_variables if trigger_request
variables
end
@@ -260,38 +260,6 @@ module Ci
Time.now - updated_at > 15.minutes.to_i
end
- ##
- # Deprecated
- #
- # This contains a hotfix for CI build data integrity, see #4246
- #
- # This method is used by `ArtifactUploader` to create a store_dir.
- # Warning: Uploader uses it after AND before file has been stored.
- #
- # This method returns old path to artifacts only if it already exists.
- #
- def artifacts_path
- # We need the project even if it's soft deleted, because whenever
- # we're really deleting the project, we'll also delete the builds,
- # and in order to delete the builds, we need to know where to find
- # the artifacts, which is depending on the data of the project.
- # We need to retain the project in this case.
- the_project = project || unscoped_project
-
- old = File.join(created_at.utc.strftime('%Y_%m'),
- the_project.ci_id.to_s,
- id.to_s)
-
- old_store = File.join(ArtifactUploader.artifacts_path, old)
- return old if the_project.ci_id && File.directory?(old_store)
-
- File.join(
- created_at.utc.strftime('%Y_%m'),
- the_project.id.to_s,
- id.to_s
- )
- end
-
def valid_token?(token)
self.token && ActiveSupport::SecurityUtils.variable_size_secure_compare(token, self.token)
end
diff --git a/app/models/ci/trigger_request.rb b/app/models/ci/trigger_request.rb
index 2b807731d0d..564334ad1ad 100644
--- a/app/models/ci/trigger_request.rb
+++ b/app/models/ci/trigger_request.rb
@@ -6,7 +6,7 @@ module Ci
belongs_to :pipeline, foreign_key: :commit_id
has_many :builds
- serialize :variables
+ serialize :variables # rubocop:disable Cop/ActiverecordSerialize
def user_variables
return [] unless variables
diff --git a/app/models/ci/variable.rb b/app/models/ci/variable.rb
index 6c6586110c5..f235260208f 100644
--- a/app/models/ci/variable.rb
+++ b/app/models/ci/variable.rb
@@ -12,11 +12,16 @@ module Ci
message: "can contain only letters, digits and '_'." }
scope :order_key_asc, -> { reorder(key: :asc) }
+ scope :unprotected, -> { where(protected: false) }
attr_encrypted :value,
mode: :per_attribute_iv_and_salt,
insecure_mode: true,
key: Gitlab::Application.secrets.db_key_base,
algorithm: 'aes-256-cbc'
+
+ def to_runner_variable
+ { key: key, value: value, public: false }
+ end
end
end
diff --git a/app/models/concerns/noteable.rb b/app/models/concerns/noteable.rb
index dd1e6630642..c7bdc997eca 100644
--- a/app/models/concerns/noteable.rb
+++ b/app/models/concerns/noteable.rb
@@ -43,7 +43,12 @@ module Noteable
end
def resolvable_discussions
- @resolvable_discussions ||= discussion_notes.resolvable.discussions(self)
+ @resolvable_discussions ||=
+ if defined?(@discussions)
+ @discussions.select(&:resolvable?)
+ else
+ discussion_notes.resolvable.discussions(self)
+ end
end
def discussions_resolvable?
diff --git a/app/models/deployment.rb b/app/models/deployment.rb
index 216cec751e3..304179c0a97 100644
--- a/app/models/deployment.rb
+++ b/app/models/deployment.rb
@@ -12,6 +12,7 @@ class Deployment < ActiveRecord::Base
delegate :name, to: :environment, prefix: true
after_create :create_ref
+ after_create :invalidate_cache
def commit
project.commit(sha)
@@ -33,6 +34,10 @@ class Deployment < ActiveRecord::Base
project.repository.create_ref(ref, ref_path)
end
+ def invalidate_cache
+ environment.expire_etag_cache
+ end
+
def manual_actions
@manual_actions ||= deployable.try(:other_actions)
end
diff --git a/app/models/diff_discussion.rb b/app/models/diff_discussion.rb
index 800574d8be0..07c4846e2ac 100644
--- a/app/models/diff_discussion.rb
+++ b/app/models/diff_discussion.rb
@@ -10,6 +10,7 @@ class DiffDiscussion < Discussion
delegate :position,
:original_position,
+ :change_position,
to: :first_note
diff --git a/app/models/diff_note.rb b/app/models/diff_note.rb
index 2a4cff37566..20ef1378500 100644
--- a/app/models/diff_note.rb
+++ b/app/models/diff_note.rb
@@ -6,9 +6,9 @@ class DiffNote < Note
NOTEABLE_TYPES = %w(MergeRequest Commit).freeze
- serialize :original_position, Gitlab::Diff::Position
- serialize :position, Gitlab::Diff::Position
- serialize :change_position, Gitlab::Diff::Position
+ serialize :original_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
+ serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
+ serialize :change_position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
validates :original_position, presence: true
validates :position, presence: true
@@ -95,13 +95,21 @@ class DiffNote < Note
return if active?
- Notes::DiffPositionUpdateService.new(
- self.project,
- nil,
+ tracer = Gitlab::Diff::PositionTracer.new(
+ project: self.project,
old_diff_refs: self.position.diff_refs,
- new_diff_refs: noteable.diff_refs,
+ new_diff_refs: self.noteable.diff_refs,
paths: self.position.paths
- ).execute(self)
+ )
+
+ result = tracer.trace(self.position)
+ return unless result
+
+ if result[:outdated]
+ self.change_position = result[:position]
+ else
+ self.position = result[:position]
+ end
end
def verify_supported
diff --git a/app/models/discussion.rb b/app/models/discussion.rb
index 0b6b920ed66..d1cec7613af 100644
--- a/app/models/discussion.rb
+++ b/app/models/discussion.rb
@@ -21,7 +21,8 @@ class Discussion
end
def self.build_collection(notes, context_noteable = nil)
- notes.group_by { |n| n.discussion_id(context_noteable) }.values.map { |notes| build(notes, context_noteable) }
+ grouped_notes = notes.group_by { |n| n.discussion_id(context_noteable) }
+ grouped_notes.values.map { |notes| build(notes, context_noteable) }
end
# Returns an alphanumeric discussion ID based on `build_discussion_id`
@@ -84,6 +85,12 @@ class Discussion
first_note.discussion_id(context_noteable)
end
+ def reply_id
+ # To reply to this discussion, we need the actual discussion_id from the database,
+ # not the potentially overwritten one based on the noteable.
+ first_note.discussion_id
+ end
+
alias_method :to_param, :id
def diff_discussion?
diff --git a/app/models/environment.rb b/app/models/environment.rb
index 61572d8d69a..6211a5c1e63 100644
--- a/app/models/environment.rb
+++ b/app/models/environment.rb
@@ -57,6 +57,10 @@ class Environment < ActiveRecord::Base
state :available
state :stopped
+
+ after_transition do |environment|
+ environment.expire_etag_cache
+ end
end
def predefined_variables
@@ -196,6 +200,18 @@ class Environment < ActiveRecord::Base
[external_url, public_path].join('/')
end
+ def expire_etag_cache
+ Gitlab::EtagCaching::Store.new.tap do |store|
+ store.touch(etag_cache_key)
+ end
+ end
+
+ def etag_cache_key
+ Gitlab::Routing.url_helpers.namespace_project_environments_path(
+ project.namespace,
+ project)
+ end
+
private
# Slugifying a name may remove the uniqueness guarantee afforded by it being
diff --git a/app/models/event.rb b/app/models/event.rb
index e6fad46077a..46e89388bc1 100644
--- a/app/models/event.rb
+++ b/app/models/event.rb
@@ -26,7 +26,7 @@ class Event < ActiveRecord::Base
belongs_to :target, polymorphic: true
# For Hash only
- serialize :data
+ serialize :data # rubocop:disable Cop/ActiverecordSerialize
# Callbacks
after_create :reset_project_activity
diff --git a/app/models/hooks/web_hook_log.rb b/app/models/hooks/web_hook_log.rb
index 2738b229d84..d73cfcf630d 100644
--- a/app/models/hooks/web_hook_log.rb
+++ b/app/models/hooks/web_hook_log.rb
@@ -1,9 +1,9 @@
class WebHookLog < ActiveRecord::Base
belongs_to :web_hook
- serialize :request_headers, Hash
- serialize :request_data, Hash
- serialize :response_headers, Hash
+ serialize :request_headers, Hash # rubocop:disable Cop/ActiverecordSerialize
+ serialize :request_data, Hash # rubocop:disable Cop/ActiverecordSerialize
+ serialize :response_headers, Hash # rubocop:disable Cop/ActiverecordSerialize
validates :web_hook, presence: true
diff --git a/app/models/legacy_diff_note.rb b/app/models/legacy_diff_note.rb
index ebf8fb92ab5..7126de2d488 100644
--- a/app/models/legacy_diff_note.rb
+++ b/app/models/legacy_diff_note.rb
@@ -7,7 +7,7 @@
class LegacyDiffNote < Note
include NoteOnDiff
- serialize :st_diff
+ serialize :st_diff # rubocop:disable Cop/ActiverecordSerialize
validates :line_code, presence: true, line_code: true
diff --git a/app/models/merge_request.rb b/app/models/merge_request.rb
index 356af776b8d..dd155252ad5 100644
--- a/app/models/merge_request.rb
+++ b/app/models/merge_request.rb
@@ -21,7 +21,7 @@ class MergeRequest < ActiveRecord::Base
belongs_to :assignee, class_name: "User"
- serialize :merge_params, Hash
+ serialize :merge_params, Hash # rubocop:disable Cop/ActiverecordSerialize
after_create :ensure_merge_request_diff, unless: :importing?
after_update :reload_diff_if_branch_changed
@@ -220,10 +220,10 @@ class MergeRequest < ActiveRecord::Base
def diffs(diff_options = {})
if compare
- # When saving MR diffs, `no_collapse` is implicitly added (because we need
+ # When saving MR diffs, `expanded` is implicitly added (because we need
# to save the entire contents to the DB), so add that here for
# consistency.
- compare.diffs(diff_options.merge(no_collapse: true))
+ compare.diffs(diff_options.merge(expanded: true))
else
merge_request_diff.diffs(diff_options)
end
@@ -421,7 +421,7 @@ class MergeRequest < ActiveRecord::Base
MergeRequests::MergeRequestDiffCacheService.new.execute(self)
new_diff_refs = self.diff_refs
- update_diff_notes_positions(
+ update_diff_discussion_positions(
old_diff_refs: old_diff_refs,
new_diff_refs: new_diff_refs,
current_user: current_user
@@ -853,19 +853,18 @@ class MergeRequest < ActiveRecord::Base
diff_refs && diff_refs.complete?
end
- def update_diff_notes_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
+ def update_diff_discussion_positions(old_diff_refs:, new_diff_refs:, current_user: nil)
return unless has_complete_diff_refs?
return if new_diff_refs == old_diff_refs
- active_diff_notes = self.notes.new_diff_notes.select do |note|
- note.active?(old_diff_refs)
+ active_diff_discussions = self.notes.new_diff_notes.discussions.select do |discussion|
+ discussion.active?(old_diff_refs)
end
+ return if active_diff_discussions.empty?
- return if active_diff_notes.empty?
+ paths = active_diff_discussions.flat_map { |n| n.diff_file.paths }.uniq
- paths = active_diff_notes.flat_map { |n| n.diff_file.paths }.uniq
-
- service = Notes::DiffPositionUpdateService.new(
+ service = Discussions::UpdateDiffPositionService.new(
self.project,
current_user,
old_diff_refs: old_diff_refs,
@@ -873,11 +872,8 @@ class MergeRequest < ActiveRecord::Base
paths: paths
)
- transaction do
- active_diff_notes.each do |note|
- service.execute(note)
- Gitlab::Timeless.timeless(note, &:save)
- end
+ active_diff_discussions.each do |discussion|
+ service.execute(discussion)
end
end
diff --git a/app/models/merge_request_diff.rb b/app/models/merge_request_diff.rb
index 1bd61c1d465..99dd2130188 100644
--- a/app/models/merge_request_diff.rb
+++ b/app/models/merge_request_diff.rb
@@ -1,7 +1,7 @@
class MergeRequestDiff < ActiveRecord::Base
include Sortable
include Importable
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Prevent store of diff if commits amount more then 500
COMMITS_SAFE_SIZE = 100
@@ -11,8 +11,8 @@ class MergeRequestDiff < ActiveRecord::Base
belongs_to :merge_request
- serialize :st_commits
- serialize :st_diffs
+ serialize :st_commits # rubocop:disable Cop/ActiverecordSerialize
+ serialize :st_diffs # rubocop:disable Cop/ActiverecordSerialize
state_machine :state, initial: :empty do
state :collected
diff --git a/app/models/note.rb b/app/models/note.rb
index 60257aac93b..832c68243fb 100644
--- a/app/models/note.rb
+++ b/app/models/note.rb
@@ -110,7 +110,7 @@ class Note < ActiveRecord::Base
end
def discussions(context_noteable = nil)
- Discussion.build_collection(fresh, context_noteable)
+ Discussion.build_collection(all.includes(:noteable).fresh, context_noteable)
end
def find_discussion(discussion_id)
diff --git a/app/models/personal_access_token.rb b/app/models/personal_access_token.rb
index e8b000ddad6..ae9f71e7747 100644
--- a/app/models/personal_access_token.rb
+++ b/app/models/personal_access_token.rb
@@ -3,7 +3,7 @@ class PersonalAccessToken < ActiveRecord::Base
include TokenAuthenticatable
add_authentication_token_field :token
- serialize :scopes, Array
+ serialize :scopes, Array # rubocop:disable Cop/ActiverecordSerialize
belongs_to :user
diff --git a/app/models/project.rb b/app/models/project.rb
index 7cb79e3249d..446329557d5 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -1245,12 +1245,19 @@ class Project < ActiveRecord::Base
variables
end
- def secret_variables
- variables.map do |variable|
- { key: variable.key, value: variable.value, public: false }
+ def secret_variables_for(ref)
+ if protected_for?(ref)
+ variables
+ else
+ variables.unprotected
end
end
+ def protected_for?(ref)
+ ProtectedBranch.protected?(self, ref) ||
+ ProtectedTag.protected?(self, ref)
+ end
+
def deployment_variables
return [] unless deployment_service
diff --git a/app/models/project_import_data.rb b/app/models/project_import_data.rb
index 331123a5a5b..e3cafd4d1c6 100644
--- a/app/models/project_import_data.rb
+++ b/app/models/project_import_data.rb
@@ -10,7 +10,7 @@ class ProjectImportData < ActiveRecord::Base
insecure_mode: true,
algorithm: 'aes-256-cbc'
- serialize :data, JSON
+ serialize :data, JSON # rubocop:disable Cop/ActiverecordSerialize
validates :project, presence: true
diff --git a/app/models/project_services/asana_service.rb b/app/models/project_services/asana_service.rb
index 3728f5642e4..9ce2d1153a7 100644
--- a/app/models/project_services/asana_service.rb
+++ b/app/models/project_services/asana_service.rb
@@ -34,7 +34,8 @@ http://app.asana.com/-/account_api'
{
type: 'text',
name: 'api_key',
- placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.'
+ placeholder: 'User Personal Access Token. User must have access to task, all comments will be attributed to this user.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/assembla_service.rb b/app/models/project_services/assembla_service.rb
index aeeff8917bf..ae6af732ed4 100644
--- a/app/models/project_services/assembla_service.rb
+++ b/app/models/project_services/assembla_service.rb
@@ -18,7 +18,7 @@ class AssemblaService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' }
]
end
diff --git a/app/models/project_services/bamboo_service.rb b/app/models/project_services/bamboo_service.rb
index 3f5b3eb159b..42939ea0ec8 100644
--- a/app/models/project_services/bamboo_service.rb
+++ b/app/models/project_services/bamboo_service.rb
@@ -47,9 +47,9 @@ class BambooService < CiService
def fields
[
{ type: 'text', name: 'bamboo_url',
- placeholder: 'Bamboo root URL like https://bamboo.example.com' },
+ placeholder: 'Bamboo root URL like https://bamboo.example.com', required: true },
{ type: 'text', name: 'build_key',
- placeholder: 'Bamboo build plan key like KEY' },
+ placeholder: 'Bamboo build plan key like KEY', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with API access, if applicable' },
{ type: 'password', name: 'password' }
diff --git a/app/models/project_services/buildkite_service.rb b/app/models/project_services/buildkite_service.rb
index 5fb95050b83..fc30f6e3365 100644
--- a/app/models/project_services/buildkite_service.rb
+++ b/app/models/project_services/buildkite_service.rb
@@ -58,11 +58,11 @@ class BuildkiteService < CiService
[
{ type: 'text',
name: 'token',
- placeholder: 'Buildkite project GitLab token' },
+ placeholder: 'Buildkite project GitLab token', required: true },
{ type: 'text',
name: 'project_url',
- placeholder: "#{ENDPOINT}/example/project" },
+ placeholder: "#{ENDPOINT}/example/project", required: true },
{ type: 'checkbox',
name: 'enable_ssl_verification',
diff --git a/app/models/project_services/campfire_service.rb b/app/models/project_services/campfire_service.rb
index 0de59af5652..c3f5b310619 100644
--- a/app/models/project_services/campfire_service.rb
+++ b/app/models/project_services/campfire_service.rb
@@ -18,7 +18,7 @@ class CampfireService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: '' },
+ { type: 'text', name: 'token', placeholder: '', required: true },
{ type: 'text', name: 'subdomain', placeholder: '' },
{ type: 'text', name: 'room', placeholder: '' }
]
@@ -76,7 +76,7 @@ class CampfireService < Service
# Returns a list of rooms, or [].
# https://github.com/basecamp/campfire-api/blob/master/sections/rooms.md#get-rooms
def rooms(auth)
- res = self.class.get("/rooms.json", auth)
+ res = self.class.get("/rooms.json", auth)
res.code == 200 ? res["rooms"] : []
end
diff --git a/app/models/project_services/chat_notification_service.rb b/app/models/project_services/chat_notification_service.rb
index 779ef54cfcb..6d1a321f651 100644
--- a/app/models/project_services/chat_notification_service.rb
+++ b/app/models/project_services/chat_notification_service.rb
@@ -21,10 +21,6 @@ class ChatNotificationService < Service
end
end
- def can_test?
- valid?
- end
-
def self.supported_events
%w[push issue confidential_issue merge_request note tag_push
pipeline wiki_page]
@@ -36,7 +32,7 @@ class ChatNotificationService < Service
def default_fields
[
- { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}" },
+ { type: 'text', name: 'webhook', placeholder: "e.g. #{webhook_placeholder}", required: true },
{ type: 'text', name: 'username', placeholder: 'e.g. GitLab' },
{ type: 'checkbox', name: 'notify_only_broken_pipelines' },
{ type: 'checkbox', name: 'notify_only_default_branch' }
diff --git a/app/models/project_services/custom_issue_tracker_service.rb b/app/models/project_services/custom_issue_tracker_service.rb
index dea915a4d05..b9e3e982b64 100644
--- a/app/models/project_services/custom_issue_tracker_service.rb
+++ b/app/models/project_services/custom_issue_tracker_service.rb
@@ -31,9 +31,9 @@ class CustomIssueTrackerService < IssueTrackerService
[
{ type: 'text', name: 'title', placeholder: title },
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
end
diff --git a/app/models/project_services/deployment_service.rb b/app/models/project_services/deployment_service.rb
index 91a55514a9a..5b8320158fc 100644
--- a/app/models/project_services/deployment_service.rb
+++ b/app/models/project_services/deployment_service.rb
@@ -30,4 +30,8 @@ class DeploymentService < Service
def terminals(environment)
raise NotImplementedError
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/drone_ci_service.rb b/app/models/project_services/drone_ci_service.rb
index 2717c240f05..f6cade9c290 100644
--- a/app/models/project_services/drone_ci_service.rb
+++ b/app/models/project_services/drone_ci_service.rb
@@ -93,8 +93,8 @@ class DroneCiService < CiService
def fields
[
- { type: 'text', name: 'token', placeholder: 'Drone CI project specific token' },
- { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com' },
+ { type: 'text', name: 'token', placeholder: 'Drone CI project specific token', required: true },
+ { type: 'text', name: 'drone_url', placeholder: 'http://drone.example.com', required: true },
{ type: 'checkbox', name: 'enable_ssl_verification', title: "Enable SSL verification" }
]
end
diff --git a/app/models/project_services/external_wiki_service.rb b/app/models/project_services/external_wiki_service.rb
index b4d7c977ce4..720ad61162e 100644
--- a/app/models/project_services/external_wiki_service.rb
+++ b/app/models/project_services/external_wiki_service.rb
@@ -19,7 +19,7 @@ class ExternalWikiService < Service
def fields
[
- { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki' }
+ { type: 'text', name: 'external_wiki_url', placeholder: 'The URL of the external Wiki', required: true }
]
end
diff --git a/app/models/project_services/flowdock_service.rb b/app/models/project_services/flowdock_service.rb
index 2a05d757eb4..2db95b9aaa3 100644
--- a/app/models/project_services/flowdock_service.rb
+++ b/app/models/project_services/flowdock_service.rb
@@ -18,7 +18,7 @@ class FlowdockService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Flowdock Git source token' }
+ { type: 'text', name: 'token', placeholder: 'Flowdock Git source token', required: true }
]
end
diff --git a/app/models/project_services/gemnasium_service.rb b/app/models/project_services/gemnasium_service.rb
index f271e1f1739..017a9b2df6e 100644
--- a/app/models/project_services/gemnasium_service.rb
+++ b/app/models/project_services/gemnasium_service.rb
@@ -18,8 +18,8 @@ class GemnasiumService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your personal API KEY on gemnasium.com ' },
- { type: 'text', name: 'token', placeholder: 'The project\'s slug on gemnasium.com' }
+ { 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
diff --git a/app/models/project_services/hipchat_service.rb b/app/models/project_services/hipchat_service.rb
index c19fed339ba..e3906943ecd 100644
--- a/app/models/project_services/hipchat_service.rb
+++ b/app/models/project_services/hipchat_service.rb
@@ -33,7 +33,7 @@ class HipchatService < Service
def fields
[
- { type: 'text', name: 'token', placeholder: 'Room token' },
+ { type: 'text', name: 'token', placeholder: 'Room token', required: true },
{ type: 'text', name: 'room', placeholder: 'Room name or ID' },
{ type: 'checkbox', name: 'notify' },
{ type: 'select', name: 'color', choices: %w(yellow red green purple gray random) },
diff --git a/app/models/project_services/irker_service.rb b/app/models/project_services/irker_service.rb
index a51d43adcb9..19357f90810 100644
--- a/app/models/project_services/irker_service.rb
+++ b/app/models/project_services/irker_service.rb
@@ -49,7 +49,7 @@ class IrkerService < Service
help: 'A default IRC URI to prepend before each recipient (optional)',
placeholder: 'irc://irc.network.net:6697/' },
{ type: 'textarea', name: 'recipients',
- placeholder: 'Recipients/channels separated by whitespaces',
+ placeholder: 'Recipients/channels separated by whitespaces', required: true,
help: 'Recipients have to be specified with a full URI: '\
'irc[s]://irc.network.net[:port]/#channel. Special cases: if '\
'you want the channel to be a nickname instead, append ",isnick" to ' \
diff --git a/app/models/project_services/issue_tracker_service.rb b/app/models/project_services/issue_tracker_service.rb
index eddf308eae3..ff138b9066d 100644
--- a/app/models/project_services/issue_tracker_service.rb
+++ b/app/models/project_services/issue_tracker_service.rb
@@ -32,9 +32,9 @@ class IssueTrackerService < Service
def fields
[
{ type: 'text', name: 'description', placeholder: description },
- { type: 'text', name: 'project_url', placeholder: 'Project url' },
- { type: 'text', name: 'issues_url', placeholder: 'Issue url' },
- { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url' }
+ { type: 'text', name: 'project_url', placeholder: 'Project url', required: true },
+ { type: 'text', name: 'issues_url', placeholder: 'Issue url', required: true },
+ { type: 'text', name: 'new_issue_url', placeholder: 'New Issue url', required: true }
]
end
diff --git a/app/models/project_services/jira_service.rb b/app/models/project_services/jira_service.rb
index 25d098b63c0..2450fb43212 100644
--- a/app/models/project_services/jira_service.rb
+++ b/app/models/project_services/jira_service.rb
@@ -86,11 +86,11 @@ class JiraService < IssueTrackerService
def fields
[
- { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com' },
+ { type: 'text', name: 'url', title: 'Web URL', placeholder: 'https://jira.example.com', required: true },
{ type: 'text', name: 'api_url', title: 'JIRA API URL', placeholder: 'If different from Web URL' },
- { type: 'text', name: 'project_key', placeholder: 'Project Key' },
- { type: 'text', name: 'username', placeholder: '' },
- { type: 'password', name: 'password', placeholder: '' },
+ { type: 'text', name: 'project_key', placeholder: 'Project Key', required: true },
+ { type: 'text', name: 'username', placeholder: '', required: true },
+ { type: 'password', name: 'password', placeholder: '', required: true },
{ type: 'text', name: 'jira_issue_transition_id', placeholder: '' }
]
end
@@ -175,10 +175,6 @@ class JiraService < IssueTrackerService
{ success: result.present?, result: result }
end
- def can_test?
- username.present? && password.present?
- end
-
# JIRA does not need test data.
# We are requesting the project that belongs to the project key.
def test_data(user = nil, project = nil)
diff --git a/app/models/project_services/mock_ci_service.rb b/app/models/project_services/mock_ci_service.rb
index 546b6e0a498..72ddf9a4be3 100644
--- a/app/models/project_services/mock_ci_service.rb
+++ b/app/models/project_services/mock_ci_service.rb
@@ -21,7 +21,8 @@ class MockCiService < CiService
[
{ type: 'text',
name: 'mock_service_url',
- placeholder: 'http://localhost:4004' }
+ placeholder: 'http://localhost:4004',
+ required: true }
]
end
@@ -79,4 +80,8 @@ class MockCiService < CiService
:error
end
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/mock_monitoring_service.rb b/app/models/project_services/mock_monitoring_service.rb
index dd04e04e198..ed0318c6b27 100644
--- a/app/models/project_services/mock_monitoring_service.rb
+++ b/app/models/project_services/mock_monitoring_service.rb
@@ -14,4 +14,8 @@ class MockMonitoringService < MonitoringService
def metrics(environment)
JSON.parse(File.read(Rails.root + 'spec/fixtures/metrics.json'))
end
+
+ def can_test?
+ false
+ end
end
diff --git a/app/models/project_services/pipelines_email_service.rb b/app/models/project_services/pipelines_email_service.rb
index f824171ad09..9d37184be2c 100644
--- a/app/models/project_services/pipelines_email_service.rb
+++ b/app/models/project_services/pipelines_email_service.rb
@@ -53,7 +53,8 @@ class PipelinesEmailService < Service
[
{ type: 'textarea',
name: 'recipients',
- placeholder: 'Emails separated by comma' },
+ placeholder: 'Emails separated by comma',
+ required: true },
{ type: 'checkbox',
name: 'notify_only_broken_pipelines' }
]
diff --git a/app/models/project_services/pivotaltracker_service.rb b/app/models/project_services/pivotaltracker_service.rb
index d86f4f6f448..f9dfa2e91c3 100644
--- a/app/models/project_services/pivotaltracker_service.rb
+++ b/app/models/project_services/pivotaltracker_service.rb
@@ -23,7 +23,8 @@ class PivotaltrackerService < Service
{
type: 'text',
name: 'token',
- placeholder: 'Pivotal Tracker API token.'
+ placeholder: 'Pivotal Tracker API token.',
+ required: true
},
{
type: 'text',
diff --git a/app/models/project_services/prometheus_service.rb b/app/models/project_services/prometheus_service.rb
index ec72cb6856d..110b8bc209b 100644
--- a/app/models/project_services/prometheus_service.rb
+++ b/app/models/project_services/prometheus_service.rb
@@ -49,7 +49,8 @@ class PrometheusService < MonitoringService
type: 'text',
name: 'api_url',
title: 'API URL',
- placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/'
+ placeholder: 'Prometheus API Base URL, like http://prometheus.example.com/',
+ required: true
}
]
end
diff --git a/app/models/project_services/pushover_service.rb b/app/models/project_services/pushover_service.rb
index fc29a5277bb..aa7bd4c3c84 100644
--- a/app/models/project_services/pushover_service.rb
+++ b/app/models/project_services/pushover_service.rb
@@ -19,10 +19,10 @@ class PushoverService < Service
def fields
[
- { type: 'text', name: 'api_key', placeholder: 'Your application key' },
- { type: 'text', name: 'user_key', placeholder: 'Your user key' },
+ { type: 'text', name: 'api_key', placeholder: 'Your application key', required: true },
+ { type: 'text', name: 'user_key', placeholder: 'Your user key', required: true },
{ type: 'text', name: 'device', placeholder: 'Leave blank for all active devices' },
- { type: 'select', name: 'priority', choices:
+ { type: 'select', name: 'priority', required: true, choices:
[
['Lowest Priority', -2],
['Low Priority', -1],
diff --git a/app/models/project_services/teamcity_service.rb b/app/models/project_services/teamcity_service.rb
index b16beb406b9..cbe137452bd 100644
--- a/app/models/project_services/teamcity_service.rb
+++ b/app/models/project_services/teamcity_service.rb
@@ -50,9 +50,9 @@ class TeamcityService < CiService
def fields
[
{ type: 'text', name: 'teamcity_url',
- placeholder: 'TeamCity root URL like https://teamcity.example.com' },
+ placeholder: 'TeamCity root URL like https://teamcity.example.com', required: true },
{ type: 'text', name: 'build_type',
- placeholder: 'Build configuration ID' },
+ placeholder: 'Build configuration ID', required: true },
{ type: 'text', name: 'username',
placeholder: 'A user with permissions to trigger a manual build' },
{ type: 'password', name: 'password' }
diff --git a/app/models/project_team.rb b/app/models/project_team.rb
index 543b9b293e0..e1cc56551ba 100644
--- a/app/models/project_team.rb
+++ b/app/models/project_team.rb
@@ -167,7 +167,7 @@ class ProjectTeam
access = RequestStore.store[key]
end
- # Lookup only the IDs we need
+ # Look up only the IDs we need
user_ids = user_ids - access.keys
return access if user_ids.empty?
@@ -178,6 +178,13 @@ class ProjectTeam
maximum(:access_level)
access.merge!(users_access)
+
+ missing_user_ids = user_ids - users_access.keys
+
+ missing_user_ids.each do |user_id|
+ access[user_id] = Gitlab::Access::NO_ACCESS
+ end
+
access
end
diff --git a/app/models/sent_notification.rb b/app/models/sent_notification.rb
index 0ae5864615a..eed3ca7e179 100644
--- a/app/models/sent_notification.rb
+++ b/app/models/sent_notification.rb
@@ -1,5 +1,5 @@
class SentNotification < ActiveRecord::Base
- serialize :position, Gitlab::Diff::Position
+ serialize :position, Gitlab::Diff::Position # rubocop:disable Cop/ActiverecordSerialize
belongs_to :project
belongs_to :noteable, polymorphic: true
diff --git a/app/models/service.rb b/app/models/service.rb
index 8916f88076e..6a0b0a5c522 100644
--- a/app/models/service.rb
+++ b/app/models/service.rb
@@ -2,7 +2,7 @@
# and implement a set of methods
class Service < ActiveRecord::Base
include Sortable
- serialize :properties, JSON
+ serialize :properties, JSON # rubocop:disable Cop/ActiverecordSerialize
default_value_for :active, false
default_value_for :push_events, true
diff --git a/app/models/user.rb b/app/models/user.rb
index 9aad327b592..e6eb9d09656 100644
--- a/app/models/user.rb
+++ b/app/models/user.rb
@@ -40,7 +40,7 @@ class User < ActiveRecord::Base
otp_secret_encryption_key: Gitlab::Application.secrets.otp_key_base
devise :two_factor_backupable, otp_number_of_backup_codes: 10
- serialize :otp_backup_codes, JSON
+ serialize :otp_backup_codes, JSON # rubocop:disable Cop/ActiverecordSerialize
devise :lockable, :recoverable, :rememberable, :trackable,
:validatable, :omniauthable, :confirmable, :registerable
@@ -101,6 +101,7 @@ class User < ActiveRecord::Base
has_many :snippets, dependent: :destroy, foreign_key: :author_id
has_many :notes, dependent: :destroy, foreign_key: :author_id
+ has_many :issues, dependent: :destroy, foreign_key: :author_id
has_many :merge_requests, dependent: :destroy, foreign_key: :author_id
has_many :events, dependent: :destroy, foreign_key: :author_id
has_many :subscriptions, dependent: :destroy
@@ -120,11 +121,6 @@ class User < ActiveRecord::Base
has_many :assigned_issues, class_name: "Issue", through: :issue_assignees, source: :issue
has_many :assigned_merge_requests, dependent: :nullify, foreign_key: :assignee_id, class_name: "MergeRequest"
- # Issues that a user owns are expected to be moved to the "ghost" user before
- # the user is destroyed. If the user owns any issues during deletion, this
- # should be treated as an exceptional condition.
- has_many :issues, dependent: :restrict_with_exception, foreign_key: :author_id
-
#
# Validations
#
@@ -369,6 +365,7 @@ class User < ActiveRecord::Base
# Pattern used to extract `@user` user references from text
def reference_pattern
%r{
+ (?<!\w)
#{Regexp.escape(reference_prefix)}
(?<user>#{Gitlab::PathRegex::FULL_NAMESPACE_FORMAT_REGEX})
}x
@@ -780,7 +777,7 @@ class User < ActiveRecord::Base
def avatar_url(size: nil, scale: 2, **args)
# We use avatar_path instead of overriding avatar_url because of carrierwave.
# See https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/11001/diffs#note_28659864
- avatar_path(args) || GravatarService.new.execute(email, size, scale)
+ avatar_path(args) || GravatarService.new.execute(email, size, scale, username: username)
end
def all_emails
diff --git a/app/serializers/entity_date_helper.rb b/app/serializers/entity_date_helper.rb
index 9607ad55a8b..71d9a65fb58 100644
--- a/app/serializers/entity_date_helper.rb
+++ b/app/serializers/entity_date_helper.rb
@@ -4,7 +4,7 @@ module EntityDateHelper
def interval_in_words(diff)
return 'Not started' unless diff
- "#{distance_of_time_in_words(Time.now, diff)} ago"
+ distance_of_time_in_words(Time.now, diff, scope: 'datetime.time_ago_in_words')
end
# Converts seconds into a hash such as:
diff --git a/app/serializers/user_entity.rb b/app/serializers/user_entity.rb
index 43754ea94f7..876512b12dc 100644
--- a/app/serializers/user_entity.rb
+++ b/app/serializers/user_entity.rb
@@ -1,2 +1,7 @@
class UserEntity < API::Entities::UserBasic
+ include RequestAwareEntity
+
+ expose :path do |user|
+ user_path(user)
+ end
end
diff --git a/app/services/ci/create_pipeline_service.rb b/app/services/ci/create_pipeline_service.rb
index 8227a78a650..13baa63220d 100644
--- a/app/services/ci/create_pipeline_service.rb
+++ b/app/services/ci/create_pipeline_service.rb
@@ -63,13 +63,10 @@ module Ci
private
def update_merge_requests_head_pipeline
- merge_requests = MergeRequest.where(source_branch: @pipeline.ref, source_project: @pipeline.project)
+ return unless pipeline.latest?
- merge_requests = merge_requests.select do |mr|
- mr.diff_head_sha == @pipeline.sha
- end
-
- MergeRequest.where(id: merge_requests).update_all(head_pipeline_id: @pipeline.id)
+ MergeRequest.where(source_project: @pipeline.project, source_branch: @pipeline.ref).
+ update_all(head_pipeline_id: @pipeline.id)
end
def skip_ci?
diff --git a/app/services/discussions/update_diff_position_service.rb b/app/services/discussions/update_diff_position_service.rb
new file mode 100644
index 00000000000..1ef8d9edbe1
--- /dev/null
+++ b/app/services/discussions/update_diff_position_service.rb
@@ -0,0 +1,41 @@
+module Discussions
+ class UpdateDiffPositionService < BaseService
+ def execute(discussion)
+ result = tracer.trace(discussion.position)
+ return unless result
+
+ position = result[:position]
+ outdated = result[:outdated]
+
+ discussion.notes.each do |note|
+ if outdated
+ note.change_position = position
+ else
+ note.position = position
+ note.change_position = nil
+ end
+ end
+
+ Note.transaction do
+ discussion.notes.each do |note|
+ Gitlab::Timeless.timeless(note, &:save)
+ end
+
+ if outdated && current_user
+ SystemNoteService.diff_discussion_outdated(discussion, project, current_user, position)
+ end
+ end
+ end
+
+ private
+
+ def tracer
+ @tracer ||= Gitlab::Diff::PositionTracer.new(
+ project: project,
+ old_diff_refs: params[:old_diff_refs],
+ new_diff_refs: params[:new_diff_refs],
+ paths: params[:paths]
+ )
+ end
+ end
+end
diff --git a/app/services/gravatar_service.rb b/app/services/gravatar_service.rb
index 433ecc2df32..e77e08aa380 100644
--- a/app/services/gravatar_service.rb
+++ b/app/services/gravatar_service.rb
@@ -1,15 +1,20 @@
class GravatarService
include Gitlab::CurrentSettings
- def execute(email, size = nil, scale = 2)
- if current_application_settings.gravatar_enabled? && email.present?
- size = 40 if size.nil? || size <= 0
+ def execute(email, size = nil, scale = 2, username: nil)
+ return unless current_application_settings.gravatar_enabled?
- sprintf gravatar_url,
- hash: Digest::MD5.hexdigest(email.strip.downcase),
- size: size * scale,
- email: email.strip
- end
+ identifier = email.presence || username.presence
+ return unless identifier
+
+ hash = Digest::MD5.hexdigest(identifier.strip.downcase)
+ size = 40 unless size && size > 0
+
+ sprintf gravatar_url,
+ hash: hash,
+ size: size * scale,
+ email: ERB::Util.url_encode(email&.strip || ''),
+ username: ERB::Util.url_encode(username&.strip || '')
end
def gitlab_config
diff --git a/app/services/merge_requests/conflicts/resolve_service.rb b/app/services/merge_requests/conflicts/resolve_service.rb
index d74a82effd6..c2c335b8461 100644
--- a/app/services/merge_requests/conflicts/resolve_service.rb
+++ b/app/services/merge_requests/conflicts/resolve_service.rb
@@ -37,11 +37,13 @@ module MergeRequests
private
def write_resolved_file_to_index(merge_index, rugged, file, params)
- new_file = if params[:sections]
- file.resolve_lines(params[:sections]).map(&:text).join("\n")
- elsif params[:content]
- file.resolve_content(params[:content])
- end
+ if params[:sections]
+ new_file = file.resolve_lines(params[:sections]).map(&:text).join("\n")
+
+ new_file << "\n" if file.our_blob.data.ends_with?("\n")
+ elsif params[:content]
+ new_file = file.resolve_content(params[:content])
+ end
our_path = file.our_path
diff --git a/app/services/merge_requests/create_service.rb b/app/services/merge_requests/create_service.rb
index fbf171f705e..71d37797bb4 100644
--- a/app/services/merge_requests/create_service.rb
+++ b/app/services/merge_requests/create_service.rb
@@ -30,15 +30,12 @@ module MergeRequests
def head_pipeline_for(merge_request)
return unless merge_request.source_project
- sha = merge_request.source_branch_head&.id
-
+ sha = merge_request.source_branch_sha
return unless sha
- pipelines =
- Ci::Pipeline.where(ref: merge_request.source_branch, project_id: merge_request.source_project.id, sha: sha).
- order(id: :desc)
+ pipelines = merge_request.source_project.pipelines.where(ref: merge_request.source_branch, sha: sha)
- pipelines.first
+ pipelines.order(id: :desc).first
end
end
end
diff --git a/app/services/notes/diff_position_update_service.rb b/app/services/notes/diff_position_update_service.rb
deleted file mode 100644
index eff7b287269..00000000000
--- a/app/services/notes/diff_position_update_service.rb
+++ /dev/null
@@ -1,33 +0,0 @@
-module Notes
- class DiffPositionUpdateService < BaseService
- def execute(note)
- results = tracer.trace(note.position)
- return unless results
-
- position = results[:position]
- outdated = results[:outdated]
-
- if outdated
- note.change_position = position
-
- if note.persisted? && current_user
- SystemNoteService.diff_discussion_outdated(note.to_discussion, project, current_user, position)
- end
- else
- note.position = position
- note.change_position = nil
- end
- end
-
- private
-
- def tracer
- @tracer ||= Gitlab::Diff::PositionTracer.new(
- project: project,
- old_diff_refs: params[:old_diff_refs],
- new_diff_refs: params[:new_diff_refs],
- paths: params[:paths]
- )
- end
- end
-end
diff --git a/app/uploaders/artifact_uploader.rb b/app/uploaders/artifact_uploader.rb
index 3e36ec91205..3bc0408f557 100644
--- a/app/uploaders/artifact_uploader.rb
+++ b/app/uploaders/artifact_uploader.rb
@@ -1,33 +1,35 @@
class ArtifactUploader < GitlabUploader
storage :file
- attr_accessor :build, :field
+ attr_reader :job, :field
- def self.artifacts_path
+ def self.local_artifacts_store
Gitlab.config.artifacts.path
end
def self.artifacts_upload_path
- File.join(self.artifacts_path, 'tmp/uploads/')
+ File.join(self.local_artifacts_store, 'tmp/uploads/')
end
- def self.artifacts_cache_path
- File.join(self.artifacts_path, 'tmp/cache/')
- end
-
- def initialize(build, field)
- @build, @field = build, field
+ def initialize(job, field)
+ @job, @field = job, field
end
def store_dir
- File.join(self.class.artifacts_path, @build.artifacts_path)
+ default_local_path
end
def cache_dir
- File.join(self.class.artifacts_cache_path, @build.artifacts_path)
+ File.join(self.class.local_artifacts_store, 'tmp/cache')
+ end
+
+ private
+
+ def default_local_path
+ File.join(self.class.local_artifacts_store, default_path)
end
- def filename
- file.try(:filename)
+ def default_path
+ File.join(job.created_at.utc.strftime('%Y_%m'), job.project_id.to_s, job.id.to_s)
end
end
diff --git a/app/uploaders/gitlab_uploader.rb b/app/uploaders/gitlab_uploader.rb
index e0a6c9b4067..02afddb8c6a 100644
--- a/app/uploaders/gitlab_uploader.rb
+++ b/app/uploaders/gitlab_uploader.rb
@@ -10,7 +10,11 @@ class GitlabUploader < CarrierWave::Uploader::Base
delegate :base_dir, to: :class
def file_storage?
- self.class.storage == CarrierWave::Storage::File
+ storage.is_a?(CarrierWave::Storage::File)
+ end
+
+ def file_cache_storage?
+ cache_storage.is_a?(CarrierWave::Storage::File)
end
# Reduce disk IO
diff --git a/app/validators/dynamic_path_validator.rb b/app/validators/dynamic_path_validator.rb
index a9b76c7c960..27ac60637fd 100644
--- a/app/validators/dynamic_path_validator.rb
+++ b/app/validators/dynamic_path_validator.rb
@@ -6,7 +6,7 @@
# Values are checked for formatting and exclusion from a list of illegal path
# names.
class DynamicPathValidator < ActiveModel::EachValidator
- extend Gitlab::Git::EncodingHelper
+ extend Gitlab::EncodingHelper
class << self
def valid_user_path?(path)
diff --git a/app/views/admin/dashboard/index.html.haml b/app/views/admin/dashboard/index.html.haml
index 53f0a1e7fde..3c9f932a225 100644
--- a/app/views/admin/dashboard/index.html.haml
+++ b/app/views/admin/dashboard/index.html.haml
@@ -79,6 +79,12 @@
= gitlab_pages
%span.light.pull-right
= boolean_to_icon gitlab_pages_enabled
+ - gitlab_shared_runners = 'Shared Runners'
+ - gitlab_shared_runners_enabled = Gitlab.config.gitlab_ci.shared_runners_enabled
+ %p{ "aria-label" => "#{gitlab_shared_runners}: status " + (gitlab_shared_runners_enabled ? "on" : "off") }
+ = gitlab_shared_runners
+ %span.light.pull-right
+ = boolean_to_icon gitlab_shared_runners_enabled
.col-md-4
%h4
diff --git a/app/views/admin/users/_user.html.haml b/app/views/admin/users/_user.html.haml
index 8862455688f..46d2e3b3de1 100644
--- a/app/views/admin/users/_user.html.haml
+++ b/app/views/admin/users/_user.html.haml
@@ -34,7 +34,7 @@
- if user.access_locked?
%li
= link_to 'Unlock', unlock_admin_user_path(user), method: :put, class: 'btn-grouped btn btn-xs btn-success', data: { confirm: 'Are you sure?' }
- - if user.can_be_removed? && can?(current_user, :destroy_user, @user)
+ - if user.can_be_removed? && can?(current_user, :destroy_user, user)
%li.divider
%li
= link_to 'Delete user', [:admin, user], data: { confirm: "USER #{user.name} WILL BE REMOVED! All issues, merge requests and groups linked to this user will also be removed! Consider cancelling this deletion and blocking the user instead. Are you sure?" },
diff --git a/app/views/discussions/_jump_to_next.html.haml b/app/views/discussions/_jump_to_next.html.haml
index 69bd416c4de..3db509f24a5 100644
--- a/app/views/discussions/_jump_to_next.html.haml
+++ b/app/views/discussions/_jump_to_next.html.haml
@@ -3,7 +3,7 @@
%jump-to-discussion{ "inline-template" => true, ":discussion-id" => "'#{discussion.try(:id)}'" }
.btn-group{ role: "group", "v-show" => "!allResolved", "v-if" => "showButton" }
%button.btn.btn-default.discussion-next-btn.has-tooltip{ "@click" => "jumpToNextUnresolvedDiscussion",
- title: "Jump to next unresolved discussion",
- "aria-label" => "Jump to next unresolved discussion",
+ ":title" => "buttonText",
+ ":aria-label" => "buttonText",
data: { container: "body" } }
= custom_icon("next_discussion")
diff --git a/app/views/projects/blame/show.html.haml b/app/views/projects/blame/show.html.haml
index a2ec3d44185..a6ee2b2f7b8 100644
--- a/app/views/projects/blame/show.html.haml
+++ b/app/views/projects/blame/show.html.haml
@@ -1,5 +1,5 @@
- @no_container = true
-- page_title "Blame", @blob.path, @ref
+- page_title "Annotate", @blob.path, @ref
= render "projects/commits/head"
%div{ class: container_class }
diff --git a/app/views/projects/blob/_breadcrumb.html.haml b/app/views/projects/blob/_breadcrumb.html.haml
index 3f58e8d232f..0ad9f258e48 100644
--- a/app/views/projects/blob/_breadcrumb.html.haml
+++ b/app/views/projects/blob/_breadcrumb.html.haml
@@ -10,7 +10,7 @@
= link_to 'Normal view', namespace_project_blob_path(@project.namespace, @project, @id),
class: 'btn'
- else
- = link_to 'Blame', namespace_project_blame_path(@project.namespace, @project, @id),
+ = link_to 'Annotate', namespace_project_blame_path(@project.namespace, @project, @id),
class: 'btn js-blob-blame-link' unless blob.empty?
= link_to 'History', namespace_project_commits_path(@project.namespace, @project, @id),
diff --git a/app/views/projects/diffs/_content.html.haml b/app/views/projects/diffs/_content.html.haml
index c7e22a0b4ec..59844bc00cd 100644
--- a/app/views/projects/diffs/_content.html.haml
+++ b/app/views/projects/diffs/_content.html.haml
@@ -3,7 +3,7 @@
.diff-content
- if diff_file.too_large?
.nothing-here-block This diff could not be displayed because it is too large.
- - elsif blob.too_large?
+ - elsif blob.truncated?
.nothing-here-block The file could not be displayed because it is too large.
- elsif blob.readable_text?
- if !diff_file.repository.diffable?(blob)
diff --git a/app/views/projects/diffs/_diffs.html.haml b/app/views/projects/diffs/_diffs.html.haml
index 4768438c29e..d538c4c86c8 100644
--- a/app/views/projects/diffs/_diffs.html.haml
+++ b/app/views/projects/diffs/_diffs.html.haml
@@ -5,8 +5,8 @@
.content-block.oneline-block.files-changed
.inline-parallel-buttons
- - if !expand_all_diffs? && diff_files.any? { |diff_file| diff_file.collapsed? }
- = link_to 'Expand all', url_for(params.merge(expand_all_diffs: 1, format: nil)), class: 'btn btn-default'
+ - if !diffs_expanded? && diff_files.any? { |diff_file| diff_file.collapsed? }
+ = link_to 'Expand all', url_for(params.merge(expanded: 1, format: nil)), class: 'btn btn-default'
- if show_whitespace_toggle
- if current_controller?(:commit)
= commit_diff_whitespace_link(diffs.project, @commit, class: 'hidden-xs')
diff --git a/app/views/projects/diffs/_line.html.haml b/app/views/projects/diffs/_line.html.haml
index 7439b8a66f7..43708d22a0c 100644
--- a/app/views/projects/diffs/_line.html.haml
+++ b/app/views/projects/diffs/_line.html.haml
@@ -3,7 +3,7 @@
- discussions = local_assigns.fetch(:discussions, nil)
- type = line.type
- line_code = diff_file.line_code(line)
-- if discussions && !line.meta?
+- if discussions && line.discussable?
- line_discussions = discussions[line_code]
%tr.line_holder{ class: type, id: (line_code unless plain) }
- case type
diff --git a/app/views/projects/jobs/_sidebar.html.haml b/app/views/projects/jobs/_sidebar.html.haml
index 3e83142377b..f700b5c9455 100644
--- a/app/views/projects/jobs/_sidebar.html.haml
+++ b/app/views/projects/jobs/_sidebar.html.haml
@@ -130,6 +130,3 @@
= build.id
- if build.retried?
%i.fa.fa-refresh.has-tooltip{ data: { container: 'body', placement: 'bottom' }, title: 'Job was retried' }
-
-:javascript
- new Sidebar();
diff --git a/app/views/projects/pipelines/_info.html.haml b/app/views/projects/pipelines/_info.html.haml
index 8607da8fcdd..673c3370b62 100644
--- a/app/views/projects/pipelines/_info.html.haml
+++ b/app/views/projects/pipelines/_info.html.haml
@@ -1,18 +1,4 @@
-.page-content-header
- .header-main-content
- = render 'ci/status/badge', status: @pipeline.detailed_status(current_user), title: @pipeline.status_title
- %strong Pipeline ##{@pipeline.id}
- triggered #{time_ago_with_tooltip(@pipeline.created_at)}
- - if @pipeline.user
- by
- = user_avatar(user: @pipeline.user, size: 24)
- = user_link(@pipeline.user)
- .header-action-buttons
- - if can?(current_user, :update_pipeline, @pipeline.project)
- - if @pipeline.retryable?
- = link_to "Retry", retry_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), class: 'js-retry-button btn btn-inverted-secondary', method: :post
- - if @pipeline.cancelable?
- = link_to "Cancel running", cancel_namespace_project_pipeline_path(@pipeline.project.namespace, @pipeline.project, @pipeline.id), data: { confirm: 'Are you sure?' }, class: 'btn btn-danger', method: :post
+#js-pipeline-header-vue.pipeline-header-container
- if @commit
.commit-box
diff --git a/app/views/projects/pipelines_settings/_show.html.haml b/app/views/projects/pipelines_settings/_show.html.haml
index 1b1910b5c0f..3b17daeb6da 100644
--- a/app/views/projects/pipelines_settings/_show.html.haml
+++ b/app/views/projects/pipelines_settings/_show.html.haml
@@ -42,7 +42,7 @@
= f.label :build_timeout_in_minutes, 'Timeout', class: 'label-light'
= f.number_field :build_timeout_in_minutes, class: 'form-control', min: '0'
%p.help-block
- Per job in minutes. If a job passes this threshold, it will be marked as failed.
+ Per job in minutes. If a job passes this threshold, it will be marked as failed
= link_to icon('question-circle'), help_page_path('user/project/pipelines/settings', anchor: 'timeout'), target: '_blank'
%hr
diff --git a/app/views/projects/registry/repositories/index.html.haml b/app/views/projects/registry/repositories/index.html.haml
index be128e92fa7..5661af01302 100644
--- a/app/views/projects/registry/repositories/index.html.haml
+++ b/app/views/projects/registry/repositories/index.html.haml
@@ -1,26 +1,60 @@
- page_title "Container Registry"
-%hr
-
-%ul.content-list
- %li.light.prepend-top-default
+.row.prepend-top-default.append-bottom-default
+ .col-lg-3
+ %h4.prepend-top-0
+ = page_title
%p
- A 'container image' is a snapshot of a container.
- You can host your container images with GitLab.
- %br
- To start using container images hosted on GitLab you first need to login:
- %pre
- %code
+ With the Docker Container Registry integrated into GitLab, every project
+ can have its own space to store its Docker images.
+ %p.append-bottom-0
+ = succeed '.' do
+ Learn more about
+ = link_to 'Container Registry', help_page_path('user/project/container_registry'), target: '_blank'
+
+ .col-lg-9
+ .panel.panel-default
+ .panel-heading
+ %h4.panel-title
+ How to use the Container Registry
+ .panel-body
+ %p
+ First log in to GitLab&rsquo;s Container Registry using your GitLab username
+ and password. If you have
+ = link_to '2FA enabled', help_page_path('user/profile/account/two_factor_authentication'), target: '_blank'
+ you need to use a
+ = succeed ':' do
+ = link_to 'personal access token', help_page_path('user/profile/account/two_factor_authentication', anchor: 'personal-access-tokens'), target: '_blank'
+ %pre
docker login #{Gitlab.config.registry.host_port}
- %br
- Then you are free to create and upload a container image with build and push commands:
- %pre
- docker build -t #{escape_once(@project.container_registry_url)}/image .
%br
- docker push #{escape_once(@project.container_registry_url)}/image
+ %p
+ Once you log in, you&rsquo;re free to create and upload a container image
+ using the common
+ %code build
+ and
+ %code push
+ commands:
+ %pre
+ :plain
+ docker build -t #{escape_once(@project.container_registry_url)} .
+ docker push #{escape_once(@project.container_registry_url)}
- - if @images.blank?
- .nothing-here-block No container image repositories in Container Registry for this project.
+ %hr
+ %h5.prepend-top-default
+ Use different image names
+ %p.light
+ GitLab supports up to 3 levels of image names. The following
+ examples of images are valid for your project:
+ %pre
+ :plain
+ #{escape_once(@project.container_registry_url)}:tag
+ #{escape_once(@project.container_registry_url)}/optional-image-name:tag
+ #{escape_once(@project.container_registry_url)}/optional-name/optional-image-name:tag
- - else
- = render partial: 'image', collection: @images
+ - if @images.blank?
+ %p.settings-message.text-center.append-bottom-default
+ No container images stored for this project. Add one by following the
+ instructions above.
+ - else
+ = render partial: 'image', collection: @images
diff --git a/app/views/projects/services/_form.html.haml b/app/views/projects/services/_form.html.haml
index f1a80f1d5e1..9167789a69d 100644
--- a/app/views/projects/services/_form.html.haml
+++ b/app/views/projects/services/_form.html.haml
@@ -1,3 +1,6 @@
+- content_for :page_specific_javascripts do
+ = webpack_bundle_tag('integrations')
+
.row.prepend-top-default.append-bottom-default
.col-lg-3
%h4.prepend-top-0
@@ -6,15 +9,17 @@
%p= @service.description
.col-lg-9
- = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'form-horizontal' }) do |form|
+ = form_for(@service, as: :service, url: namespace_project_service_path(@project.namespace, @project, @service.to_param), method: :put, html: { class: 'gl-show-field-errors form-horizontal js-integration-settings-form', data: { 'can-test' => @service.can_test?, 'test-url' => test_namespace_project_service_path } }) do |form|
= render 'shared/service_settings', form: form, subject: @service
.footer-block.row-content-block
- = form.submit 'Save changes', class: 'btn btn-save'
+ %button.btn.btn-save{ type: 'submit' }
+ = icon('spinner spin', class: 'hidden js-btn-spinner')
+ %span.js-btn-label
+ Save changes
&nbsp;
- if @service.valid? && @service.activated?
- unless @service.can_test?
- disabled_class = 'disabled'
- disabled_title = @service.disabled_title
- = link_to 'Test settings', test_namespace_project_service_path(@project.namespace, @project, @service), class: "btn #{disabled_class}", title: disabled_title
- = link_to "Cancel", namespace_project_settings_integrations_path(@project.namespace, @project), class: "btn btn-cancel"
+ = link_to 'Cancel', namespace_project_settings_integrations_path(@project.namespace, @project), class: 'btn btn-cancel'
diff --git a/app/views/projects/variables/_content.html.haml b/app/views/projects/variables/_content.html.haml
index 06477aba103..98f618ca3b8 100644
--- a/app/views/projects/variables/_content.html.haml
+++ b/app/views/projects/variables/_content.html.haml
@@ -1,7 +1,8 @@
%h4.prepend-top-0
- Secret Variables
+ Secret variables
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'secret-variables'), target: '_blank'
%p
- These variables will be set to environment by the runner.
+ These variables will be set to environment by the runner, and could be protected by exposing only to protected branches or tags.
%p
So you can use them for passwords, secret keys or whatever you want.
%p
diff --git a/app/views/projects/variables/_form.html.haml b/app/views/projects/variables/_form.html.haml
index 1ae86d258af..0a70a301cb4 100644
--- a/app/views/projects/variables/_form.html.haml
+++ b/app/views/projects/variables/_form.html.haml
@@ -7,4 +7,13 @@
.form-group
= f.label :value, "Value", class: "label-light"
= f.text_area :value, class: "form-control", placeholder: "PROJECT_VARIABLE"
+ .form-group
+ .checkbox
+ = f.label :protected do
+ = f.check_box :protected
+ %strong Protected
+ .help-block
+ This variable will be passed only to pipelines running on protected branches and tags
+ = link_to icon('question-circle'), help_page_path('ci/variables/README', anchor: 'protected-secret-variables'), target: '_blank'
+
= f.submit btn_text, class: "btn btn-save"
diff --git a/app/views/projects/variables/_table.html.haml b/app/views/projects/variables/_table.html.haml
index 0ce597dcf21..59cd3c4b592 100644
--- a/app/views/projects/variables/_table.html.haml
+++ b/app/views/projects/variables/_table.html.haml
@@ -3,10 +3,12 @@
%colgroup
%col
%col
+ %col
%col{ width: 100 }
%thead
%th Key
%th Value
+ %th Protected
%th
%tbody
- @project.variables.order_key_asc.each do |variable|
@@ -14,6 +16,7 @@
%tr
%td.variable-key= variable.key
%td.variable-value{ "data-value" => variable.value }******
+ %td.variable-protected= Gitlab::Utils.boolean_to_yes_no(variable.protected)
%td.variable-menu
= link_to namespace_project_variable_path(@project.namespace, @project, variable), class: "btn btn-transparent btn-variable-edit" do
%span.sr-only
diff --git a/app/views/shared/_field.html.haml b/app/views/shared/_field.html.haml
index d74b0043949..795447a9ca6 100644
--- a/app/views/shared/_field.html.haml
+++ b/app/views/shared/_field.html.haml
@@ -3,6 +3,7 @@
- value = @service.send(name)
- type = field[:type]
- placeholder = field[:placeholder]
+- required = field[:required]
- choices = field[:choices]
- default_choice = field[:default_choice]
- help = field[:help]
@@ -14,14 +15,14 @@
= form.label name, title, class: "control-label"
.col-sm-10
- if type == 'text'
- = form.text_field name, class: "form-control", placeholder: placeholder
+ = form.text_field name, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'textarea'
- = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder
+ = form.text_area name, rows: 5, class: "form-control", placeholder: placeholder, required: required
- elsif type == 'checkbox'
= form.check_box name
- elsif type == 'select'
= form.select name, options_for_select(choices, value ? value : default_choice), {}, { class: "form-control" }
- elsif type == 'password'
- = form.password_field name, autocomplete: "new-password", class: "form-control"
+ = form.password_field name, autocomplete: "new-password", class: "form-control", required: value.blank? && :required
- if help
%span.help-block= help
diff --git a/app/views/shared/issuable/_search_bar.html.haml b/app/views/shared/issuable/_search_bar.html.haml
index f8d755b6961..be9f9ee29c4 100644
--- a/app/views/shared/issuable/_search_bar.html.haml
+++ b/app/views/shared/issuable/_search_bar.html.haml
@@ -26,8 +26,6 @@
%li.input-token
%input.form-control.filtered-search{ id: "filtered-search-#{type.to_s}", placeholder: 'Search or filter results...', data: { 'project-id' => @project.id, 'username-params' => @users.to_json(only: [:id, :username]), 'base-endpoint' => namespace_project_path(@project.namespace, @project) } }
= icon('filter')
- %button.clear-search.hidden{ type: 'button' }
- = icon('times')
#js-dropdown-hint.filtered-search-input-dropdown-menu.dropdown-menu.hint-dropdown
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { action: 'submit' } }
@@ -46,30 +44,27 @@
%span.js-filter-tag.dropdown-light-content
{{tag}}
#js-dropdown-author.filtered-search-input-dropdown-menu.dropdown-menu
+ - if current_user
+ %ul{ data: { dropdown: true } }
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.dropdown-user
- %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
- .dropdown-user-details
- %span
- {{name}}
- %span.dropdown-light-content
- @{{username}}
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-assignee.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
%button.btn.btn-link
No Assignee
%li.divider
+ - if current_user
+ = render 'shared/issuable/user_dropdown_item',
+ user: current_user
%ul.filter-dropdown{ data: { dynamic: true, dropdown: true } }
- %li.filter-dropdown-item
- %button.btn.btn-link.dropdown-user
- %img.avatar{ alt: '{{name}}\'s avatar', width: '30', data: { src: '{{avatar_url}}' } }
- .dropdown-user-details
- %span
- {{name}}
- %span.dropdown-light-content
- @{{username}}
+ = render 'shared/issuable/user_dropdown_item',
+ user: User.new(username: '{{username}}', name: '{{name}}'),
+ avatar: { lazy: true, url: '{{avatar_url}}' }
#js-dropdown-milestone.filtered-search-input-dropdown-menu.dropdown-menu
%ul{ data: { dropdown: true } }
%li.filter-dropdown-item{ data: { value: 'none' } }
@@ -98,6 +93,8 @@
%span.dropdown-label-box{ style: 'background: {{color}}' }
%span.label-title.js-data-value
{{title}}
+ %button.clear-search.hidden{ type: 'button' }
+ = icon('times')
.filter-dropdown-container
- if type == :boards
- if can?(current_user, :admin_list, @project)
diff --git a/app/views/shared/issuable/_user_dropdown_item.html.haml b/app/views/shared/issuable/_user_dropdown_item.html.haml
new file mode 100644
index 00000000000..a82c01c6dc2
--- /dev/null
+++ b/app/views/shared/issuable/_user_dropdown_item.html.haml
@@ -0,0 +1,11 @@
+- user = local_assigns.fetch(:user)
+- avatar = local_assigns.fetch(:avatar, { })
+
+%li.filter-dropdown-item{ class: ('js-current-user' if user == current_user) }
+ %button.btn.btn-link.dropdown-user{ type: :button }
+ = user_avatar_without_link(user: user, lazy: avatar[:lazy], url: avatar[:url], size: 30)
+ .dropdown-user-details
+ %span
+ = user.name
+ %span.dropdown-light-content
+ = user.to_reference
diff --git a/changelogs/unreleased/10378-promote-blameless-culture.yml b/changelogs/unreleased/10378-promote-blameless-culture.yml
new file mode 100644
index 00000000000..8cf64dfd793
--- /dev/null
+++ b/changelogs/unreleased/10378-promote-blameless-culture.yml
@@ -0,0 +1,4 @@
+---
+title: Changed Blame to Annotate in the UI to promote blameless culture
+merge_request: 10378
+author: Ilya Vassilevsky
diff --git a/changelogs/unreleased/24196-protected-variables.yml b/changelogs/unreleased/24196-protected-variables.yml
new file mode 100644
index 00000000000..71567a9d794
--- /dev/null
+++ b/changelogs/unreleased/24196-protected-variables.yml
@@ -0,0 +1,5 @@
+---
+title: Add protected variables which would only be passed to protected branches or
+ protected tags
+merge_request: 11688
+author:
diff --git a/changelogs/unreleased/28080-system-checks.yml b/changelogs/unreleased/28080-system-checks.yml
new file mode 100644
index 00000000000..7d83014279a
--- /dev/null
+++ b/changelogs/unreleased/28080-system-checks.yml
@@ -0,0 +1,4 @@
+---
+title: Refactored gitlab:app:check into SystemCheck liberary and improve some checks
+merge_request: 9173
+author:
diff --git a/changelogs/unreleased/30651-improve-container-registry-description.yml b/changelogs/unreleased/30651-improve-container-registry-description.yml
new file mode 100644
index 00000000000..0157c9885bc
--- /dev/null
+++ b/changelogs/unreleased/30651-improve-container-registry-description.yml
@@ -0,0 +1,4 @@
+---
+title: Add changelog for improved Registry description
+merge_request: 11816
+author:
diff --git a/changelogs/unreleased/31511-jira-settings.yml b/changelogs/unreleased/31511-jira-settings.yml
new file mode 100644
index 00000000000..4f9ddb13ef6
--- /dev/null
+++ b/changelogs/unreleased/31511-jira-settings.yml
@@ -0,0 +1,4 @@
+---
+title: Simplify testing and saving service integrations
+merge_request: 11599
+author:
diff --git a/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml b/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml
new file mode 100644
index 00000000000..00957f7e4f7
--- /dev/null
+++ b/changelogs/unreleased/31602-display-whether-shared-runner-is-enabled-in-the-admin-dashboard.yml
@@ -0,0 +1,4 @@
+---
+title: Display Shared Runner status in Admin Dashboard
+merge_request: 11783
+author: Ivan Chernov
diff --git a/changelogs/unreleased/31644-make-cookie-sessions-unique.yml b/changelogs/unreleased/31644-make-cookie-sessions-unique.yml
new file mode 100644
index 00000000000..e9a6a32cf70
--- /dev/null
+++ b/changelogs/unreleased/31644-make-cookie-sessions-unique.yml
@@ -0,0 +1,4 @@
+---
+title: Update session cookie key name to be unique to instance in development
+merge_request:
+author:
diff --git a/changelogs/unreleased/31849-pipeline-real-time-header.yml b/changelogs/unreleased/31849-pipeline-real-time-header.yml
new file mode 100644
index 00000000000..2bb7af897ff
--- /dev/null
+++ b/changelogs/unreleased/31849-pipeline-real-time-header.yml
@@ -0,0 +1,4 @@
+---
+title: Makes header information of pipeline show page realtine
+merge_request:
+author:
diff --git a/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml b/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml
new file mode 100644
index 00000000000..eca42176501
--- /dev/null
+++ b/changelogs/unreleased/32983-merge-conflict-resolution-removed-the-newline-in-the-end-of-file.yml
@@ -0,0 +1,4 @@
+---
+title: Keep trailing newline when resolving conflicts by picking sides
+merge_request:
+author:
diff --git a/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml b/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml
new file mode 100644
index 00000000000..5eb4e15e311
--- /dev/null
+++ b/changelogs/unreleased/33207-show-delete-option-in-admin-users-page.yml
@@ -0,0 +1,4 @@
+---
+title: Allow admins to delete users from the admin users page
+merge_request: 11852
+author:
diff --git a/changelogs/unreleased/33215-fix-hard-delete-of-users.yml b/changelogs/unreleased/33215-fix-hard-delete-of-users.yml
new file mode 100644
index 00000000000..29699ff745a
--- /dev/null
+++ b/changelogs/unreleased/33215-fix-hard-delete-of-users.yml
@@ -0,0 +1,4 @@
+---
+title: Fix hard-deleting users when they have authored issues
+merge_request: 11855
+author:
diff --git a/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml b/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml
new file mode 100644
index 00000000000..c33278998ee
--- /dev/null
+++ b/changelogs/unreleased/33242-create-project-for-user-api-ignores-path-parameter.yml
@@ -0,0 +1,4 @@
+---
+title: Fix missing optional path parameter in "Create project for user" API
+merge_request: 11868
+author:
diff --git a/changelogs/unreleased/aliyun-backup-provider.yml b/changelogs/unreleased/aliyun-backup-provider.yml
new file mode 100644
index 00000000000..e7505e44a59
--- /dev/null
+++ b/changelogs/unreleased/aliyun-backup-provider.yml
@@ -0,0 +1,4 @@
+---
+title: Add Aliyun OSS as the backup storage provider
+merge_request: 9721
+author: Yuanfei Zhu
diff --git a/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml b/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml
new file mode 100644
index 00000000000..0306663ac8d
--- /dev/null
+++ b/changelogs/unreleased/bugfix-v3-deploy_keys-can_push.yml
@@ -0,0 +1,4 @@
+---
+title: "Fixed handling of the `can_push` attribute in the v3 deploy_keys api"
+merge_request: 11607
+author: Richard Clamp
diff --git a/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml b/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml
new file mode 100644
index 00000000000..50db66c89ba
--- /dev/null
+++ b/changelogs/unreleased/dm-comment-on-mr-commit-discussion.yml
@@ -0,0 +1,4 @@
+---
+title: Fix replying to a commit discussion displayed in the context of an MR
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-discussions-n-plus-1.yml b/changelogs/unreleased/dm-discussions-n-plus-1.yml
new file mode 100644
index 00000000000..b97e4344248
--- /dev/null
+++ b/changelogs/unreleased/dm-discussions-n-plus-1.yml
@@ -0,0 +1,4 @@
+---
+title: Resolve N+1 query issue with discussions
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-emails-are-not-user-references.yml b/changelogs/unreleased/dm-emails-are-not-user-references.yml
new file mode 100644
index 00000000000..fe55a75a88f
--- /dev/null
+++ b/changelogs/unreleased/dm-emails-are-not-user-references.yml
@@ -0,0 +1,4 @@
+---
+title: Don't match email addresses or foo@bar as user references
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-fix-jump-button.yml b/changelogs/unreleased/dm-fix-jump-button.yml
new file mode 100644
index 00000000000..4cde354fa28
--- /dev/null
+++ b/changelogs/unreleased/dm-fix-jump-button.yml
@@ -0,0 +1,4 @@
+---
+title: Fix title of discussion jump button at top of page
+merge_request:
+author:
diff --git a/changelogs/unreleased/dm-gravatar-username.yml b/changelogs/unreleased/dm-gravatar-username.yml
new file mode 100644
index 00000000000..d50455061ec
--- /dev/null
+++ b/changelogs/unreleased/dm-gravatar-username.yml
@@ -0,0 +1,4 @@
+---
+title: Add username parameter to gravatar URL
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml b/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml
new file mode 100644
index 00000000000..c2671a96b83
--- /dev/null
+++ b/changelogs/unreleased/fix-n-plus-one-queries-for-user-access.yml
@@ -0,0 +1,4 @@
+---
+title: Fix N+1 queries for non-members in comment threads
+merge_request:
+author:
diff --git a/changelogs/unreleased/fix_diff_line_comments.yml b/changelogs/unreleased/fix_diff_line_comments.yml
new file mode 100644
index 00000000000..bdb0539b49d
--- /dev/null
+++ b/changelogs/unreleased/fix_diff_line_comments.yml
@@ -0,0 +1,5 @@
+---
+title: 'Fix: A diff comment on a change at last line of a file shows as two comments
+ in discussion'
+merge_request:
+author:
diff --git a/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml b/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml
new file mode 100644
index 00000000000..df4de9f4e21
--- /dev/null
+++ b/changelogs/unreleased/jouve-gitlab-ce-admin_keys.yml
@@ -0,0 +1,5 @@
+---
+title: Redirect to user's keys index instead of user's index after a key is deleted
+ in the admin
+merge_request: 10227
+author: Cyril Jouve
diff --git a/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml b/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml
new file mode 100644
index 00000000000..bd022a3a91b
--- /dev/null
+++ b/changelogs/unreleased/migrate-artifacts-to-a-new-path.yml
@@ -0,0 +1,4 @@
+---
+title: Migrate artifacts to a new path
+merge_request:
+author:
diff --git a/changelogs/unreleased/winh-current-user-filter.yml b/changelogs/unreleased/winh-current-user-filter.yml
new file mode 100644
index 00000000000..e5409827b31
--- /dev/null
+++ b/changelogs/unreleased/winh-current-user-filter.yml
@@ -0,0 +1,4 @@
+---
+title: Show current user immediately in issuable filters
+merge_request: 11630
+author:
diff --git a/changelogs/unreleased/winh-styled-people-search-bar.yml b/changelogs/unreleased/winh-styled-people-search-bar.yml
new file mode 100644
index 00000000000..a088af37d8d
--- /dev/null
+++ b/changelogs/unreleased/winh-styled-people-search-bar.yml
@@ -0,0 +1,4 @@
+---
+title: Style people in issuable search bar
+merge_request: 11402
+author:
diff --git a/changelogs/unreleased/zj-drop-fk-if-exists.yml b/changelogs/unreleased/zj-drop-fk-if-exists.yml
new file mode 100644
index 00000000000..237ba936de9
--- /dev/null
+++ b/changelogs/unreleased/zj-drop-fk-if-exists.yml
@@ -0,0 +1,4 @@
+---
+title: Remove foreigh key on ci_trigger_schedules only if it exists
+merge_request:
+author:
diff --git a/changelogs/unreleased/zj-realtime-env-list.yml b/changelogs/unreleased/zj-realtime-env-list.yml
new file mode 100644
index 00000000000..6460d17edc9
--- /dev/null
+++ b/changelogs/unreleased/zj-realtime-env-list.yml
@@ -0,0 +1,4 @@
+---
+title: Make environment table realtime
+merge_request: 11333
+author:
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 6c1c1f8c041..d2aeb66ebf6 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -169,7 +169,7 @@ production: &base
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
- # gravatar urls: possible placeholders: %{hash} %{size} %{email}
+ # gravatar urls: possible placeholders: %{hash} %{size} %{email} %{username}
# plain_url: "http://..." # default: http://www.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
# ssl_url: "https://..." # default: https://secure.gravatar.com/avatar/%{hash}?s=%{size}&d=identicon
diff --git a/config/initializers/ar_monkey_patch.rb b/config/initializers/active_record_locking.rb
index 9266ff0f615..9266ff0f615 100644
--- a/config/initializers/ar_monkey_patch.rb
+++ b/config/initializers/active_record_locking.rb
diff --git a/config/initializers/active_record_preloader.rb b/config/initializers/active_record_preloader.rb
new file mode 100644
index 00000000000..3b16014f302
--- /dev/null
+++ b/config/initializers/active_record_preloader.rb
@@ -0,0 +1,15 @@
+module ActiveRecord
+ module Associations
+ class Preloader
+ module NoCommitPreloader
+ def preloader_for(reflection, owners, rhs_klass)
+ return NullPreloader if rhs_klass == ::Commit
+
+ super
+ end
+ end
+
+ prepend NoCommitPreloader
+ end
+ end
+end
diff --git a/config/initializers/session_store.rb b/config/initializers/session_store.rb
index 70be2617cab..8919f7640fe 100644
--- a/config/initializers/session_store.rb
+++ b/config/initializers/session_store.rb
@@ -10,6 +10,12 @@ rescue
Settings.gitlab['session_expire_delay'] ||= 10080
end
+cookie_key = if Rails.env.development?
+ "_gitlab_session_#{Digest::SHA256.hexdigest(Rails.root.to_s)}"
+ else
+ "_gitlab_session"
+ end
+
if Rails.env.test?
Gitlab::Application.config.session_store :cookie_store, key: "_gitlab_session"
else
@@ -19,7 +25,7 @@ else
Gitlab::Application.config.session_store(
:redis_store, # Using the cookie_store would enable session replay attacks.
servers: redis_config,
- key: '_gitlab_session',
+ key: cookie_key,
secure: Gitlab.config.gitlab.https,
httponly: true,
expires_in: Settings.gitlab['session_expire_delay'] * 60,
diff --git a/config/karma.config.js b/config/karma.config.js
index eb082dd28bf..40c58e7771d 100644
--- a/config/karma.config.js
+++ b/config/karma.config.js
@@ -13,6 +13,8 @@ if (webpackConfig.plugins) {
});
}
+webpackConfig.devtool = 'cheap-inline-source-map';
+
// Karma configuration
module.exports = function(config) {
var progressReporter = process.env.CI ? 'mocha' : 'progress';
diff --git a/config/locales/en.yml b/config/locales/en.yml
index 12a59be79f0..9d47425950a 100644
--- a/config/locales/en.yml
+++ b/config/locales/en.yml
@@ -13,3 +13,39 @@ en:
pagination:
previous: "Prev"
next: "Next"
+ datetime:
+ time_ago_in_words:
+ half_a_minute: "half a minute ago"
+ less_than_x_seconds:
+ one: "less than 1 second ago"
+ other: "less than %{count} seconds ago"
+ x_seconds:
+ one: "1 second ago"
+ other: "%{count} seconds ago"
+ less_than_x_minutes:
+ one: "less than a minute ago"
+ other: "less than %{count} minutes ago"
+ x_minutes:
+ one: "1 minute ago"
+ other: "%{count} minutes ago"
+ about_x_hours:
+ one: "about 1 hour ago"
+ other: "about %{count} hours ago"
+ x_days:
+ one: "1 day ago"
+ other: "%{count} days ago"
+ about_x_months:
+ one: "about 1 month ago"
+ other: "about %{count} months ago"
+ x_months:
+ one: "1 month ago"
+ other: "%{count} months ago"
+ about_x_years:
+ one: "about 1 year ago"
+ other: "about %{count} years ago"
+ over_x_years:
+ one: "over 1 year ago"
+ other: "over %{count} years ago"
+ almost_x_years:
+ one: "almost 1 year ago"
+ other: "almost %{count} years ago"
diff --git a/config/locales/es.yml b/config/locales/es.yml
index 87e79beee74..0f9dc39535d 100644
--- a/config/locales/es.yml
+++ b/config/locales/es.yml
@@ -61,6 +61,41 @@ es:
- :month
- :year
datetime:
+ time_ago_in_words:
+ half_a_minute: "hace medio minuto"
+ less_than_x_seconds:
+ one: "hace menos de 1 segundo"
+ other: "hace menos de %{count} segundos"
+ x_seconds:
+ one: "hace 1 segundo"
+ other: "hace %{count} segundos"
+ less_than_x_minutes:
+ one: "hace menos de un minuto"
+ other: "hace menos de %{count} minutos"
+ x_minutes:
+ one: "hace 1 minuto"
+ other: "hace %{count} minutos"
+ about_x_hours:
+ one: "hace alrededor de 1 hora"
+ other: "hace alrededor de %{count} horas"
+ x_days:
+ one: "hace un día"
+ other: "hace %{count} días"
+ about_x_months:
+ one: "hace alrededor de 1 mes"
+ other: "hace alrededor de %{count} meses"
+ x_months:
+ one: "hace 1 mes"
+ other: "hace %{count} meses"
+ about_x_years:
+ one: "hace alrededor de 1 año"
+ other: "hace alrededor de %{count} años"
+ over_x_years:
+ one: "hace más de 1 año"
+ other: "hace %{count} años"
+ almost_x_years:
+ one: "hace casi 1 año"
+ other: "hace casi %{count} años"
distance_in_words:
about_x_hours:
one: alrededor de 1 hora
diff --git a/config/routes/project.rb b/config/routes/project.rb
index 5aac44fce10..14718e2f3c4 100644
--- a/config/routes/project.rb
+++ b/config/routes/project.rb
@@ -67,7 +67,7 @@ constraints(ProjectUrlConstrainer.new) do
resources :services, constraints: { id: /[^\/]+/ }, only: [:index, :edit, :update] do
member do
- get :test
+ put :test
end
end
diff --git a/config/webpack.config.js b/config/webpack.config.js
index c77b1d6334c..c99298239b2 100644
--- a/config/webpack.config.js
+++ b/config/webpack.config.js
@@ -41,6 +41,7 @@ var config = {
group: './group.js',
groups_list: './groups_list.js',
issue_show: './issue_show/index.js',
+ integrations: './integrations',
locale: './locale/index.js',
main: './main.js',
merge_conflicts: './merge_conflicts/merge_conflicts_bundle.js',
@@ -74,8 +75,6 @@ var config = {
chunkFilename: IS_PRODUCTION ? '[name].[chunkhash].chunk.js' : '[name].chunk.js',
},
- devtool: 'cheap-module-source-map',
-
module: {
rules: [
{
diff --git a/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb b/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb
index 6116ca59ee4..1587eee06ae 100644
--- a/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb
+++ b/db/migrate/20170425112628_remove_foreigh_key_ci_trigger_schedules.rb
@@ -4,10 +4,20 @@ class RemoveForeighKeyCiTriggerSchedules < ActiveRecord::Migration
DOWNTIME = false
def up
- remove_foreign_key :ci_trigger_schedules, column: :trigger_id
+ if fk_on_trigger_schedules?
+ remove_foreign_key :ci_trigger_schedules, column: :trigger_id
+ end
end
def down
# no op, the foreign key should not have been here
end
+
+ private
+
+ # Not made more generic and lifted to the helpers as Rails 5 will provide
+ # such an API
+ def fk_on_trigger_schedules?
+ connection.foreign_keys(:ci_trigger_schedules).include?("ci_triggers")
+ end
end
diff --git a/db/migrate/20170524161101_add_protected_to_ci_variables.rb b/db/migrate/20170524161101_add_protected_to_ci_variables.rb
new file mode 100644
index 00000000000..99d4861e889
--- /dev/null
+++ b/db/migrate/20170524161101_add_protected_to_ci_variables.rb
@@ -0,0 +1,15 @@
+class AddProtectedToCiVariables < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ def up
+ add_column_with_default(:ci_variables, :protected, :boolean, default: false)
+ end
+
+ def down
+ remove_column(:ci_variables, :protected)
+ end
+end
diff --git a/db/post_migrate/20170523083112_migrate_old_artifacts.rb b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
new file mode 100644
index 00000000000..f2690bd0017
--- /dev/null
+++ b/db/post_migrate/20170523083112_migrate_old_artifacts.rb
@@ -0,0 +1,72 @@
+class MigrateOldArtifacts < ActiveRecord::Migration
+ include Gitlab::Database::MigrationHelpers
+
+ DOWNTIME = false
+
+ disable_ddl_transaction!
+
+ # This uses special heuristic to find potential candidates for data migration
+ # Read more about this here: https://gitlab.com/gitlab-org/gitlab-ce/issues/32036#note_30422345
+
+ def up
+ builds_with_artifacts.find_each do |build|
+ build.migrate_artifacts!
+ end
+ end
+
+ def down
+ end
+
+ private
+
+ def builds_with_artifacts
+ Build.with_artifacts
+ .joins('JOIN projects ON projects.id = ci_builds.project_id')
+ .where('ci_builds.id < ?', min_id)
+ .where('projects.ci_id IS NOT NULL')
+ .select('id', 'created_at', 'project_id', 'projects.ci_id AS ci_id')
+ end
+
+ def min_id
+ Build.joins('JOIN projects ON projects.id = ci_builds.project_id')
+ .where('projects.ci_id IS NULL')
+ .pluck('coalesce(min(ci_builds.id), 0)')
+ .first
+ end
+
+ class Build < ActiveRecord::Base
+ self.table_name = 'ci_builds'
+
+ scope :with_artifacts, -> { where.not(artifacts_file: [nil, '']) }
+
+ def migrate_artifacts!
+ return unless File.exist?(source_artifacts_path)
+ return if File.exist?(target_artifacts_path)
+
+ ensure_target_path
+
+ FileUtils.move(source_artifacts_path, target_artifacts_path)
+ end
+
+ private
+
+ def source_artifacts_path
+ @source_artifacts_path ||=
+ File.join(Gitlab.config.artifacts.path,
+ created_at.utc.strftime('%Y_%m'),
+ ci_id.to_s, id.to_s)
+ end
+
+ def target_artifacts_path
+ @target_artifacts_path ||=
+ File.join(Gitlab.config.artifacts.path,
+ created_at.utc.strftime('%Y_%m'),
+ project_id.to_s, id.to_s)
+ end
+
+ def ensure_target_path
+ directory = File.dirname(target_artifacts_path)
+ FileUtils.mkdir_p(directory) unless Dir.exist?(directory)
+ end
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index bac8f95ce3b..fa1c5dc15c4 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -356,6 +356,7 @@ ActiveRecord::Schema.define(version: 20170525174156) do
t.string "encrypted_value_salt"
t.string "encrypted_value_iv"
t.integer "project_id", null: false
+ t.boolean "protected", default: false, null: false
end
add_index "ci_variables", ["project_id"], name: "index_ci_variables_on_project_id", using: :btree
@@ -1492,4 +1493,4 @@ ActiveRecord::Schema.define(version: 20170525174156) do
add_foreign_key "trending_projects", "projects", on_delete: :cascade
add_foreign_key "u2f_registrations", "users"
add_foreign_key "web_hook_logs", "web_hooks", on_delete: :cascade
-end \ No newline at end of file
+end
diff --git a/doc/api/build_variables.md b/doc/api/build_variables.md
index 2aaf1c93705..d4f00256ed3 100644
--- a/doc/api/build_variables.md
+++ b/doc/api/build_variables.md
@@ -61,11 +61,12 @@ Create a new build variable.
POST /projects/:id/variables
```
-| Attribute | Type | required | Description |
-|-----------|---------|----------|-----------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
-| `value` | string | yes | The `value` of a variable |
+| Attribute | Type | required | Description |
+|-------------|---------|----------|-----------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable; must have no more than 255 characters; only `A-Z`, `a-z`, `0-9`, and `_` are allowed |
+| `value` | string | yes | The `value` of a variable |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables" --form "key=NEW_VARIABLE" --form "value=new value"
@@ -74,7 +75,8 @@ curl --request POST --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitl
```json
{
"key": "NEW_VARIABLE",
- "value": "new value"
+ "value": "new value",
+ "protected": false
}
```
@@ -86,11 +88,12 @@ Update a project's build variable.
PUT /projects/:id/variables/:key
```
-| Attribute | Type | required | Description |
-|-----------|---------|----------|-------------------------|
-| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
-| `key` | string | yes | The `key` of a variable |
-| `value` | string | yes | The `value` of a variable |
+| Attribute | Type | required | Description |
+|-------------|---------|----------|-------------------------|
+| `id` | integer/string | yes | The ID of a project or [urlencoded NAMESPACE/PROJECT_NAME of the project](README.md#namespaced-path-encoding) owned by the authenticated user |
+| `key` | string | yes | The `key` of a variable |
+| `value` | string | yes | The `value` of a variable |
+| `protected` | boolean | no | Whether the variable is protected |
```
curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitlab.example.com/api/v4/projects/1/variables/NEW_VARIABLE" --form "value=updated value"
@@ -99,7 +102,8 @@ curl --request PUT --header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" "https://gitla
```json
{
"key": "NEW_VARIABLE",
- "value": "updated value"
+ "value": "updated value",
+ "protected": true
}
```
diff --git a/doc/ci/variables/README.md b/doc/ci/variables/README.md
index 0d4d08106f8..76ad7c564a3 100644
--- a/doc/ci/variables/README.md
+++ b/doc/ci/variables/README.md
@@ -10,7 +10,7 @@ The variables can be overwritten and they take precedence over each other in
this order:
1. [Trigger variables][triggers] (take precedence over all)
-1. [Secret variables](#secret-variables)
+1. [Secret variables](#secret-variables) or [protected secret variables](#protected-secret-variables)
1. YAML-defined [job-level variables](../yaml/README.md#job-variables)
1. YAML-defined [global variables](../yaml/README.md#variables)
1. [Deployment variables](#deployment-variables)
@@ -153,9 +153,25 @@ storing things like passwords, secret keys and credentials.
Secret variables can be added by going to your project's
**Settings âž” Pipelines**, then finding the section called
-**Secret Variables**.
+**Secret variables**.
-Once you set them, they will be available for all subsequent jobs.
+Once you set them, they will be available for all subsequent pipelines.
+
+## Protected secret variables
+
+>**Notes:**
+This feature requires GitLab 9.3 or higher.
+
+Secret variables could be protected. Whenever a secret variable is
+protected, it would only be securely passed to pipelines running on the
+[protected branches] or [protected tags]. The other pipelines would not get any
+protected variables.
+
+Protected variables can be added by going to your project's
+**Settings âž” Pipelines**, then finding the section called
+**Secret variables**, and check *Protected*.
+
+Once you set them, they will be available for all subsequent pipelines.
## Deployment variables
@@ -385,3 +401,5 @@ export CI_REGISTRY_PASSWORD="longalfanumstring"
[runner]: https://docs.gitlab.com/runner/
[triggered]: ../triggers/README.md
[triggers]: ../triggers/README.md#pass-job-variables-to-a-trigger
+[protected branches]: ../../user/project/protected_branches.md
+[protected tags]: ../../user/project/protected_tags.md
diff --git a/doc/customization/libravatar.md b/doc/customization/libravatar.md
index c46ce2ee203..9bd22d3966d 100644
--- a/doc/customization/libravatar.md
+++ b/doc/customization/libravatar.md
@@ -16,7 +16,7 @@ the configuration options as follows:
```yml
gravatar:
enabled: true
- # gravatar URLs: possible placeholders: %{hash} %{size} %{email}
+ # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
plain_url: "http://cdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
```
@@ -25,7 +25,7 @@ the configuration options as follows:
```yml
gravatar:
enabled: true
- # gravatar URLs: possible placeholders: %{hash} %{size} %{email}
+ # gravatar URLs: possible placeholders: %{hash} %{size} %{email} %{username}
ssl_url: "https://seccdn.libravatar.org/avatar/%{hash}?s=%{size}&d=identicon"
```
diff --git a/doc/development/README.md b/doc/development/README.md
index be013667684..af4131c4a8f 100644
--- a/doc/development/README.md
+++ b/doc/development/README.md
@@ -50,6 +50,7 @@
- [Adding database indexes](adding_database_indexes.md)
- [Post Deployment Migrations](post_deployment_migrations.md)
- [Foreign Keys & Associations](foreign_keys.md)
+- [Serializing Data](serializing_data.md)
## i18n
diff --git a/doc/development/i18n_guide.md b/doc/development/i18n_guide.md
index 735345bd126..bfb0779fbfa 100644
--- a/doc/development/i18n_guide.md
+++ b/doc/development/i18n_guide.md
@@ -233,8 +233,7 @@ Let's suppose you want to add translations for a new language, let's say French.
containing the translations:
```sh
- bundle exec rake gettext:pack
- bundle exec rake gettext:po_to_json
+ bundle exec rake gettext:compile
```
1. In order to see the translated content we need to change our preferred language
diff --git a/doc/development/serializing_data.md b/doc/development/serializing_data.md
new file mode 100644
index 00000000000..2b56f48bc44
--- /dev/null
+++ b/doc/development/serializing_data.md
@@ -0,0 +1,84 @@
+# Serializing Data
+
+**Summary:** don't store serialized data in the database, use separate columns
+and/or tables instead.
+
+Rails makes it possible to store serialized data in JSON, YAML or other formats.
+Such a field can be defined as follows:
+
+```ruby
+class Issue < ActiveRecord::Model
+ serialize :custom_fields
+end
+```
+
+While it may be tempting to store serialized data in the database there are many
+problems with this. This document will outline these problems and provide an
+alternative.
+
+## Serialized Data Is Less Powerful
+
+When using a relational database you have the ability to query individual
+fields, change the schema, index data and so forth. When you use serialized data
+all of that becomes either very difficult or downright impossible. While
+PostgreSQL does offer the ability to query JSON fields it is mostly meant for
+very specialized use cases, and not for more general use. If you use YAML in
+turn there's no way to query the data at all.
+
+## Waste Of Space
+
+Storing serialized data such as JSON or YAML will end up wasting a lot of space.
+This is because these formats often include additional characters (e.g. double
+quotes or newlines) besides the data that you are storing.
+
+## Difficult To Manage
+
+There comes a time where you will need to add a new field to the serialized
+data, or change an existing one. Using serialized data this becomes difficult
+and very time consuming as the only way of doing so is to re-write all the
+stored values. To do so you would have to:
+
+1. Retrieve the data
+1. Parse it into a Ruby structure
+1. Mutate it
+1. Serialize it back to a String
+1. Store it in the database
+
+On the other hand, if one were to use regular columns adding a column would be
+as easy as this:
+
+```sql
+ALTER TABLE table_name ADD COLUMN column_name type;
+```
+
+Such a query would take very little to no time and would immediately apply to
+all rows, without having to re-write large JSON or YAML structures.
+
+Finally, there comes a time when the JSON or YAML structure is no longer
+sufficient and you need to migrate away from it. When storing only a few rows
+this may not be a problem, but when storing millions of rows such a migration
+can easily take hours or even days to complete.
+
+## Relational Databases Are Not Document Stores
+
+When storing data as JSON or YAML you're essentially using your database as if
+it were a document store (e.g. MongoDB), except you're not using any of the
+powerful features provided by a typical RDBMS _nor_ are you using any of the
+features provided by a typical document store (e.g. the ability to index fields
+of documents with variable fields). In other words, it's a waste.
+
+## Consistent Fields
+
+One argument sometimes made in favour of serialized data is having to store
+widely varying fields and values. Sometimes this is truly the case, and then
+perhaps it might make sense to use serialized data. However, in 99% of the cases
+the fields and types stored tend to be the same for every row. Even if there is
+a slight difference you can still use separate columns and just not set the ones
+you don't need.
+
+## The Solution
+
+The solution is very simple: just use separate columns and/or separate tables.
+This will allow you to use all the features provided by your database, it will
+make it easier to manage and migrate the data, you'll conserve space, you can
+index the data efficiently and so forth.
diff --git a/doc/install/installation.md b/doc/install/installation.md
index af21d99d024..c911b297f8d 100644
--- a/doc/install/installation.md
+++ b/doc/install/installation.md
@@ -505,6 +505,10 @@ Check if GitLab and its environment are configured correctly:
sudo -u git -H yarn install --production --pure-lockfile
sudo -u git -H bundle exec rake gitlab:assets:compile RAILS_ENV=production NODE_ENV=production
+### Compile GetText PO files
+
+ sudo -u git -H bundle exec rake gettext:compile RAILS_ENV=production
+
### Start Your GitLab Instance
sudo service gitlab start
diff --git a/doc/raketasks/backup_restore.md b/doc/raketasks/backup_restore.md
index 5be6053b76e..855f437cd73 100644
--- a/doc/raketasks/backup_restore.md
+++ b/doc/raketasks/backup_restore.md
@@ -133,7 +133,7 @@ It uses the [Fog library](http://fog.io/) to perform the upload.
In the example below we use Amazon S3 for storage, but Fog also lets you use
[other storage providers](http://fog.io/storage/). GitLab
[imports cloud drivers](https://gitlab.com/gitlab-org/gitlab-ce/blob/30f5b9a5b711b46f1065baf755e413ceced5646b/Gemfile#L88)
-for AWS, Google, OpenStack Swift and Rackspace as well. A local driver is
+for AWS, Google, OpenStack Swift, Rackspace and Aliyun as well. A local driver is
[also available](#uploading-to-locally-mounted-shares).
For omnibus packages, add the following to `/etc/gitlab/gitlab.rb`:
diff --git a/doc/user/project/container_registry.md b/doc/user/project/container_registry.md
index 6a2ca7fb428..3cbb0b5196d 100644
--- a/doc/user/project/container_registry.md
+++ b/doc/user/project/container_registry.md
@@ -95,8 +95,6 @@ and click **Registry** in the project menu.
This view will show you all tags in your project and will easily allow you to
delete them.
-![Container Registry panel](img/container_registry_panel.png)
-
## Build and push images using GitLab CI
> **Note:**
diff --git a/doc/user/project/img/container_registry_panel.png b/doc/user/project/img/container_registry_panel.png
deleted file mode 100644
index e4c9ecbb25b..00000000000
--- a/doc/user/project/img/container_registry_panel.png
+++ /dev/null
Binary files differ
diff --git a/doc/user/project/pipelines/schedules.md b/doc/user/project/pipelines/schedules.md
index 641876f948f..d19d184f9b0 100644
--- a/doc/user/project/pipelines/schedules.md
+++ b/doc/user/project/pipelines/schedules.md
@@ -53,7 +53,7 @@ Sidekiq, which runs according to its interval. For example, if you set a
schedule to create a pipeline every minute (`* * * * *`) and the Sidekiq worker
runs on 00:00 and 12:00 every day (`0 */12 * * *`), only 2 pipelines will be
created per day. To change the Sidekiq worker's frequency, you have to edit the
-`trigger_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab.
+`pipeline_schedule_worker_cron` value in your `gitlab.rb` and restart GitLab.
For GitLab.com, you can check the [dedicated settings page][settings]. If you
don't have admin access to the server, ask your administrator.
diff --git a/doc/workflow/gitlab_flow.md b/doc/workflow/gitlab_flow.md
index 1b172b21f3d..e10ccc4fc46 100644
--- a/doc/workflow/gitlab_flow.md
+++ b/doc/workflow/gitlab_flow.md
@@ -67,7 +67,7 @@ With GitLab flow we offer additional guidance for these questions.
![Master branch and production branch with arrow that indicate deployments](production_branch.png)
GitHub flow does assume you are able to deploy to production every time you merge a feature branch.
-This is possible for SaaS applications but there are many cases where this is not possible.
+This is possible for e.g. SaaS applications, but there are many cases where this is not possible.
One would be a situation where you are not in control of the exact release moment, for example an iOS application that needs to pass App Store validation.
Another example is when you have deployment windows (workdays from 10am to 4pm when the operations team is at full capacity) but you also merge code at other times.
In these cases you can make a production branch that reflects the deployed code.
@@ -134,7 +134,7 @@ If the assigned person does not feel comfortable they can close the merge reques
In GitLab it is common to protect the long-lived branches (e.g. the master branch) so that normal developers [can't modify these protected branches](http://docs.gitlab.com/ce/permissions/permissions.html).
So if you want to merge it into a protected branch you assign it to someone with master authorizations.
-## Issues with GitLab flow
+## Issue tracking with GitLab flow
![Merge request with the branch name 15-require-a-password-to-change-it and assignee field shown](merge_request.png)
@@ -173,9 +173,9 @@ It is possible that one feature branch solves more than one issue.
![Merge request showing the linked issues that will be closed](close_issue_mr.png)
-Linking to the issue can happen by mentioning them from commit messages (fixes #14, closes #67, etc.) or from the merge request description.
-In GitLab this creates a comment in the issue that the merge requests mentions the issue.
-And the merge request shows the linked issues.
+Linking to issues can happen by mentioning them in commit messages (fixes #14, closes #67, etc.) or in the merge request description.
+GitLab then creates links to the mentioned issues and creates comments in the corresponding issues linking back to the merge request.
+
These issues are closed once code is merged into the default branch.
If you only want to make the reference without closing the issue you can also just mention it: "Duck typing is preferred. #12".
@@ -300,7 +300,7 @@ If there are no merge conflicts and the feature branches are short lived the ris
If there are merge conflicts you merge the master branch into the feature branch and the CI server will rerun the tests.
If you have long lived feature branches that last for more than a few days you should make your issues smaller.
-## Merging in other code
+## Working wih feature branches
![Shell output showing git pull output](git_pull.png)
diff --git a/features/project/service.feature b/features/project/service.feature
index cce5f58adec..54f07ebca92 100644
--- a/features/project/service.feature
+++ b/features/project/service.feature
@@ -11,77 +11,77 @@ Feature: Project Services
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings
- Then I should see hipchat service settings saved
+ Then I should see the Hipchat success message
Scenario: Activate hipchat service with custom server
When I visit project "Shop" services page
And I click hipchat service link
And I fill hipchat settings with custom server
- Then I should see hipchat service settings with custom server saved
+ Then I should see the Hipchat success message
Scenario: Activate pivotaltracker service
When I visit project "Shop" services page
And I click pivotaltracker service link
And I fill pivotaltracker settings
- Then I should see pivotaltracker service settings saved
+ Then I should see the Pivotaltracker success message
Scenario: Activate Flowdock service
When I visit project "Shop" services page
And I click Flowdock service link
And I fill Flowdock settings
- Then I should see Flowdock service settings saved
+ Then I should see the Flowdock success message
Scenario: Activate Assembla service
When I visit project "Shop" services page
And I click Assembla service link
And I fill Assembla settings
- Then I should see Assembla service settings saved
+ Then I should see the Assembla success message
Scenario: Activate Slack notifications service
When I visit project "Shop" services page
And I click Slack notifications service link
And I fill Slack notifications settings
- Then I should see Slack Notifications service settings saved
+ Then I should see the Slack notifications success message
Scenario: Activate Pushover service
When I visit project "Shop" services page
And I click Pushover service link
And I fill Pushover settings
- Then I should see Pushover service settings saved
+ Then I should see the Pushover success message
Scenario: Activate email on push service
When I visit project "Shop" services page
And I click email on push service link
And I fill email on push settings
- Then I should see email on push service settings saved
+ Then I should see the Emails on push success message
Scenario: Activate JIRA service
When I visit project "Shop" services page
And I click jira service link
And I fill jira settings
- Then I should see jira service settings saved
+ Then I should see the JIRA success message
Scenario: Activate Irker (IRC Gateway) service
When I visit project "Shop" services page
And I click Irker service link
And I fill Irker settings
- Then I should see Irker service settings saved
+ Then I should see the Irker success message
Scenario: Activate Atlassian Bamboo CI service
When I visit project "Shop" services page
And I click Atlassian Bamboo CI service link
And I fill Atlassian Bamboo CI settings
- Then I should see Atlassian Bamboo CI service settings saved
+ Then I should see the Bamboo success message
And I should see empty field Change Password
Scenario: Activate jetBrains TeamCity CI service
When I visit project "Shop" services page
And I click jetBrains TeamCity CI service link
And I fill jetBrains TeamCity CI settings
- Then I should see jetBrains TeamCity CI service settings saved
+ Then I should see the JetBrains success message
Scenario: Activate Asana service
When I visit project "Shop" services page
And I click Asana service link
And I fill Asana settings
- Then I should see Asana service settings saved
+ Then I should see the Asana success message
diff --git a/features/steps/project/services.rb b/features/steps/project/services.rb
index 66368a159ec..6bac4df16f8 100644
--- a/features/steps/project/services.rb
+++ b/features/steps/project/services.rb
@@ -34,8 +34,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings saved' do
- expect(find_field('Room').value).to eq 'gitlab'
+ step 'I should see the Hipchat success message' do
+ expect(page).to have_content 'HipChat activated.'
end
step 'I fill hipchat settings with custom server' do
@@ -46,10 +46,6 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see hipchat service settings with custom server saved' do
- expect(find_field('Server').value).to eq 'https://chat.example.com'
- end
-
step 'I click pivotaltracker service link' do
click_link 'PivotalTracker'
end
@@ -60,8 +56,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see pivotaltracker service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Pivotaltracker success message' do
+ expect(page).to have_content 'PivotalTracker activated.'
end
step 'I click Flowdock service link' do
@@ -74,8 +70,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Flowdock service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Flowdock success message' do
+ expect(page).to have_content 'Flowdock activated.'
end
step 'I click Assembla service link' do
@@ -88,8 +84,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Assembla service settings saved' do
- expect(find_field('Token').value).to eq 'verySecret'
+ step 'I should see the Assembla success message' do
+ expect(page).to have_content 'Assembla activated.'
end
step 'I click Asana service link' do
@@ -103,9 +99,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Asana service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('Restrict to branch').value).to eq 'master'
+ step 'I should see the Asana success message' do
+ expect(page).to have_content 'Asana activated.'
end
step 'I click email on push service link' do
@@ -113,12 +108,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill email on push settings' do
+ check 'Active'
fill_in 'Recipients', with: 'qa@company.name'
click_button 'Save'
end
- step 'I should see email on push service settings saved' do
- expect(find_field('Recipients').value).to eq 'qa@company.name'
+ step 'I should see the Emails on push success message' do
+ expect(page).to have_content 'Emails on push activated.'
end
step 'I click Irker service link' do
@@ -132,9 +128,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Irker service settings saved' do
- expect(find_field('Recipients').value).to eq 'irc://chat.freenode.net/#commits'
- expect(find_field('Colorize messages').value).to eq '1'
+ step 'I should see the Irker success message' do
+ expect(page).to have_content 'Irker (IRC gateway) activated.'
end
step 'I click Slack notifications service link' do
@@ -147,8 +142,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Slack Notifications service settings saved' do
- expect(find_field('Webhook').value).to eq 'https://hooks.slack.com/services/SVRWFV0VVAR97N/B02R25XN3/ZBqu7xMupaEEICInN685'
+ step 'I should see the Slack notifications success message' do
+ expect(page).to have_content 'Slack notifications activated.'
end
step 'I click Pushover service link' do
@@ -165,12 +160,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Pushover service settings saved' do
- expect(find_field('Api key').value).to eq 'verySecret'
- expect(find_field('User key').value).to eq 'verySecret'
- expect(find_field('Device').value).to eq 'myDevice'
- expect(find_field('Priority').find('option[selected]').value).to eq '1'
- expect(find_field('Sound').find('option[selected]').value).to eq 'bike'
+ step 'I should see the Pushover success message' do
+ expect(page).to have_content 'Pushover activated.'
end
step 'I click jira service link' do
@@ -178,6 +169,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
end
step 'I fill jira settings' do
+ check 'Active'
+
fill_in 'Web URL', with: 'http://jira.example'
fill_in 'JIRA API URL', with: 'http://jira.example/api'
fill_in 'Username', with: 'gitlab'
@@ -186,11 +179,8 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see jira service settings saved' do
- expect(find_field('Web URL').value).to eq 'http://jira.example'
- expect(find_field('JIRA API URL').value).to eq 'http://jira.example/api'
- expect(find_field('Username').value).to eq 'gitlab'
- expect(find_field('Project Key').value).to eq 'GITLAB'
+ step 'I should see the JIRA success message' do
+ expect(page).to have_content 'JIRA activated.'
end
step 'I click Atlassian Bamboo CI service link' do
@@ -206,13 +196,13 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see Atlassian Bamboo CI service settings saved' do
- expect(find_field('Bamboo url').value).to eq 'http://bamboo.example.com'
- expect(find_field('Build key').value).to eq 'KEY'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the Bamboo success message' do
+ expect(page).to have_content 'Atlassian Bamboo CI activated.'
end
step 'I should see empty field Change Password' do
+ click_link 'Atlassian Bamboo CI'
+
expect(find_field('Enter new password').value).to be_nil
end
@@ -229,9 +219,7 @@ class Spinach::Features::ProjectServices < Spinach::FeatureSteps
click_button 'Save'
end
- step 'I should see JetBrains TeamCity CI service settings saved' do
- expect(find_field('Teamcity url').value).to eq 'http://teamcity.example.com'
- expect(find_field('Build type').value).to eq 'GitlabTest_Build'
- expect(find_field('Username').value).to eq 'user'
+ step 'I should see the JetBrains success message' do
+ expect(page).to have_content 'JetBrains TeamCity CI activated.'
end
end
diff --git a/features/steps/project/source/browse_files.rb b/features/steps/project/source/browse_files.rb
index 6efd4374b32..d099d7af167 100644
--- a/features/steps/project/source/browse_files.rb
+++ b/features/steps/project/source/browse_files.rb
@@ -372,6 +372,7 @@ class Spinach::Features::ProjectSourceBrowseFiles < Spinach::FeatureSteps
expect(page).to have_content 'Permalink'
expect(page).not_to have_content 'Edit'
expect(page).not_to have_content 'Blame'
+ expect(page).not_to have_content 'Annotate'
expect(page).to have_content 'Delete'
expect(page).to have_content 'Replace'
end
diff --git a/lib/api/commits.rb b/lib/api/commits.rb
index 621b9dcecd9..c6fc17cc391 100644
--- a/lib/api/commits.rb
+++ b/lib/api/commits.rb
@@ -176,7 +176,7 @@ module API
}
if params[:path]
- commit.raw_diffs(all_diffs: true).each do |diff|
+ commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
diff --git a/lib/api/entities.rb b/lib/api/entities.rb
index fc8183a62c1..31da85e9917 100644
--- a/lib/api/entities.rb
+++ b/lib/api/entities.rb
@@ -331,7 +331,7 @@ module API
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: Entities::RepoDiff do |compare, _|
- compare.raw_diffs(all_diffs: true).to_a
+ compare.raw_diffs(limits: false).to_a
end
end
@@ -344,7 +344,7 @@ module API
expose :commits, using: Entities::RepoCommit
expose :diffs, using: Entities::RepoDiff do |compare, _|
- compare.raw_diffs(all_diffs: true).to_a
+ compare.raw_diffs(limits: false).to_a
end
end
@@ -548,7 +548,7 @@ module API
end
expose :diffs, using: Entities::RepoDiff do |compare, options|
- compare.diffs(all_diffs: true).to_a
+ compare.diffs(limits: false).to_a
end
expose :compare_timeout do |compare, options|
@@ -675,6 +675,7 @@ module API
class Variable < Grape::Entity
expose :key, :value
+ expose :protected?, as: :protected
end
class Pipeline < PipelineBasic
diff --git a/lib/api/helpers.rb b/lib/api/helpers.rb
index d61450f8258..81f6fc3201d 100644
--- a/lib/api/helpers.rb
+++ b/lib/api/helpers.rb
@@ -311,6 +311,16 @@ module API
end
end
+ def present_artifacts!(artifacts_file)
+ return not_found! unless artifacts_file.exists?
+
+ if artifacts_file.file_storage?
+ present_file!(artifacts_file.path, artifacts_file.filename)
+ else
+ redirect_to(artifacts_file.url)
+ end
+ end
+
private
def private_token
diff --git a/lib/api/jobs.rb b/lib/api/jobs.rb
index 0223957fde1..8a67de10bca 100644
--- a/lib/api/jobs.rb
+++ b/lib/api/jobs.rb
@@ -224,16 +224,6 @@ module API
find_build(id) || not_found!
end
- def present_artifacts!(artifacts_file)
- if !artifacts_file.file_storage?
- redirect_to(build.artifacts_file.url)
- elsif artifacts_file.exists?
- present_file!(artifacts_file.path, artifacts_file.filename)
- else
- not_found!
- end
- end
-
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
diff --git a/lib/api/projects.rb b/lib/api/projects.rb
index d00d4fe1737..17008aa6d0f 100644
--- a/lib/api/projects.rb
+++ b/lib/api/projects.rb
@@ -129,6 +129,7 @@ module API
params do
requires :name, type: String, desc: 'The name of the project'
requires :user_id, type: Integer, desc: 'The ID of a user'
+ optional :path, type: String, desc: 'The path of the repository'
optional :default_branch, type: String, desc: 'The default branch of the project'
use :optional_params
use :create_params
diff --git a/lib/api/runner.rb b/lib/api/runner.rb
index 6fbb02cb3aa..3fd0536dadd 100644
--- a/lib/api/runner.rb
+++ b/lib/api/runner.rb
@@ -241,16 +241,7 @@ module API
get '/:id/artifacts' do
job = authenticate_job!
- artifacts_file = job.artifacts_file
- unless artifacts_file.file_storage?
- return redirect_to job.artifacts_file.url
- end
-
- unless artifacts_file.exists?
- not_found!
- end
-
- present_file!(artifacts_file.path, artifacts_file.filename)
+ present_artifacts!(job.artifacts_file)
end
end
end
diff --git a/lib/api/v3/builds.rb b/lib/api/v3/builds.rb
index 21935922414..93ad9eb26b8 100644
--- a/lib/api/v3/builds.rb
+++ b/lib/api/v3/builds.rb
@@ -225,16 +225,6 @@ module API
find_build(id) || not_found!
end
- def present_artifacts!(artifacts_file)
- if !artifacts_file.file_storage?
- redirect_to(build.artifacts_file.url)
- elsif artifacts_file.exists?
- present_file!(artifacts_file.path, artifacts_file.filename)
- else
- not_found!
- end
- end
-
def filter_builds(builds, scope)
return builds if scope.nil? || scope.empty?
diff --git a/lib/api/v3/commits.rb b/lib/api/v3/commits.rb
index 674de592f0a..5936f4700aa 100644
--- a/lib/api/v3/commits.rb
+++ b/lib/api/v3/commits.rb
@@ -167,7 +167,7 @@ module API
}
if params[:path]
- commit.raw_diffs(all_diffs: true).each do |diff|
+ commit.raw_diffs(limits: false).each do |diff|
next unless diff.new_path == params[:path]
lines = Gitlab::Diff::Parser.new.parse(diff.diff.each_line)
diff --git a/lib/api/v3/deploy_keys.rb b/lib/api/v3/deploy_keys.rb
index bbb174b6003..b90e2061da3 100644
--- a/lib/api/v3/deploy_keys.rb
+++ b/lib/api/v3/deploy_keys.rb
@@ -41,6 +41,7 @@ module API
params do
requires :key, type: String, desc: 'The new deploy key'
requires :title, type: String, desc: 'The name of the deploy key'
+ optional :can_push, type: Boolean, desc: "Can deploy key push to the project's repository"
end
post ":id/#{path}" do
params[:key].strip!
diff --git a/lib/api/v3/entities.rb b/lib/api/v3/entities.rb
index 2e1b243c2db..7c5065dee90 100644
--- a/lib/api/v3/entities.rb
+++ b/lib/api/v3/entities.rb
@@ -226,7 +226,7 @@ module API
class MergeRequestChanges < MergeRequest
expose :diffs, as: :changes, using: ::API::Entities::RepoDiff do |compare, _|
- compare.raw_diffs(all_diffs: true).to_a
+ compare.raw_diffs(limits: false).to_a
end
end
diff --git a/lib/api/variables.rb b/lib/api/variables.rb
index 5acde41551b..381c4ef50b0 100644
--- a/lib/api/variables.rb
+++ b/lib/api/variables.rb
@@ -42,6 +42,7 @@ module API
params do
requires :key, type: String, desc: 'The key of the variable'
requires :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: String, desc: 'Whether the variable is protected'
end
post ':id/variables' do
variable = user_project.variables.create(declared(params, include_parent_namespaces: false).to_h)
@@ -59,13 +60,14 @@ module API
params do
optional :key, type: String, desc: 'The key of the variable'
optional :value, type: String, desc: 'The value of the variable'
+ optional :protected, type: String, desc: 'Whether the variable is protected'
end
put ':id/variables/:key' do
variable = user_project.variables.find_by(key: params[:key])
return not_found!('Variable') unless variable
- if variable.update(value: params[:value])
+ if variable.update(declared_params(include_missing: false).except(:key))
present variable, with: Entities::Variable
else
render_validation_error!(variable)
diff --git a/lib/backup/artifacts.rb b/lib/backup/artifacts.rb
index 51fa3867e67..1f4bda6f588 100644
--- a/lib/backup/artifacts.rb
+++ b/lib/backup/artifacts.rb
@@ -3,7 +3,7 @@ require 'backup/files'
module Backup
class Artifacts < Files
def initialize
- super('artifacts', ArtifactUploader.artifacts_path)
+ super('artifacts', ArtifactUploader.local_artifacts_store)
end
def create_files_dir
diff --git a/lib/ci/api/builds.rb b/lib/ci/api/builds.rb
index 67b269b330c..2285ef241d7 100644
--- a/lib/ci/api/builds.rb
+++ b/lib/ci/api/builds.rb
@@ -187,14 +187,14 @@ module Ci
build = authenticate_build!
artifacts_file = build.artifacts_file
- unless artifacts_file.file_storage?
- return redirect_to build.artifacts_file.url
- end
-
unless artifacts_file.exists?
not_found!
end
+ unless artifacts_file.file_storage?
+ return redirect_to build.artifacts_file.url
+ end
+
present_file!(artifacts_file.path, artifacts_file.filename)
end
diff --git a/lib/gitlab/current_settings.rb b/lib/gitlab/current_settings.rb
index 82576d197fe..9e14b35b0f8 100644
--- a/lib/gitlab/current_settings.rb
+++ b/lib/gitlab/current_settings.rb
@@ -19,7 +19,7 @@ module Gitlab
settings = ::ApplicationSetting.last
end
- settings ||= ::ApplicationSetting.create_from_defaults unless ActiveRecord::Migrator.needs_migration?
+ settings ||= ::ApplicationSetting.create_from_defaults
end
settings || in_memory_application_settings
@@ -46,7 +46,8 @@ module Gitlab
active_db_connection = ActiveRecord::Base.connection.active? rescue false
active_db_connection &&
- ActiveRecord::Base.connection.table_exists?('application_settings')
+ ActiveRecord::Base.connection.table_exists?('application_settings') &&
+ !ActiveRecord::Migrator.needs_migration?
rescue ActiveRecord::NoDatabaseError
false
end
diff --git a/lib/gitlab/diff/file_collection/base.rb b/lib/gitlab/diff/file_collection/base.rb
index 79836a2fbab..a6007ebf531 100644
--- a/lib/gitlab/diff/file_collection/base.rb
+++ b/lib/gitlab/diff/file_collection/base.rb
@@ -7,7 +7,7 @@ module Gitlab
delegate :count, :size, :real_size, to: :diff_files
def self.default_options
- ::Commit.max_diff_options.merge(ignore_whitespace_change: false, no_collapse: false)
+ ::Commit.max_diff_options.merge(ignore_whitespace_change: false, expanded: false)
end
def initialize(diffable, project:, diff_options: nil, diff_refs: nil, fallback_diff_refs: nil)
diff --git a/lib/gitlab/diff/line.rb b/lib/gitlab/diff/line.rb
index 0a15c6d9358..bd52ae47e9f 100644
--- a/lib/gitlab/diff/line.rb
+++ b/lib/gitlab/diff/line.rb
@@ -59,6 +59,10 @@ module Gitlab
type == 'match'
end
+ def discussable?
+ !['match', 'new-nonewline', 'old-nonewline'].include?(type)
+ end
+
def as_json(opts = nil)
{
type: type,
diff --git a/lib/gitlab/email/message/repository_push.rb b/lib/gitlab/email/message/repository_push.rb
index 6c69cd9e6a9..ea035e33eff 100644
--- a/lib/gitlab/email/message/repository_push.rb
+++ b/lib/gitlab/email/message/repository_push.rb
@@ -42,7 +42,7 @@ module Gitlab
return unless compare
# This diff is more moderated in number of files and lines
- @diffs ||= compare.diffs(max_files: 30, max_lines: 5000, no_collapse: true).diff_files
+ @diffs ||= compare.diffs(max_files: 30, max_lines: 5000, expanded: true).diff_files
end
def diffs_count
diff --git a/lib/gitlab/encoding_helper.rb b/lib/gitlab/encoding_helper.rb
new file mode 100644
index 00000000000..dbe28e6bb93
--- /dev/null
+++ b/lib/gitlab/encoding_helper.rb
@@ -0,0 +1,62 @@
+module Gitlab
+ module EncodingHelper
+ extend self
+
+ # This threshold is carefully tweaked to prevent usage of encodings detected
+ # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
+ # we're better off sticking with utf8 encoding.
+ # Reason: git diff can return strings with invalid utf8 byte sequences if it
+ # truncates a diff in the middle of a multibyte character. In this case
+ # CharlockHolmes will try to guess the encoding and will likely suggest an
+ # obscure encoding with low confidence.
+ # There is a lot more info with this merge request:
+ # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
+ ENCODING_CONFIDENCE_THRESHOLD = 40
+
+ def encode!(message)
+ return nil unless message.respond_to? :force_encoding
+
+ # if message is utf-8 encoding, just return it
+ message.force_encoding("UTF-8")
+ return message if message.valid_encoding?
+
+ # return message if message type is binary
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ return message.force_encoding("BINARY") if detect && detect[:type] == :binary
+
+ # force detected encoding if we have sufficient confidence.
+ if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
+ message.force_encoding(detect[:encoding])
+ end
+
+ # encode and clean the bad chars
+ message.replace clean(message)
+ rescue
+ encoding = detect ? detect[:encoding] : "unknown"
+ "--broken encoding: #{encoding}"
+ end
+
+ def encode_utf8(message)
+ detect = CharlockHolmes::EncodingDetector.detect(message)
+ if detect
+ begin
+ CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
+ rescue ArgumentError => e
+ Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
+
+ ''
+ end
+ else
+ clean(message)
+ end
+ end
+
+ private
+
+ def clean(message)
+ message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
+ .encode("UTF-8")
+ .gsub("\0".encode("UTF-8"), "")
+ end
+ end
+end
diff --git a/lib/gitlab/etag_caching/router.rb b/lib/gitlab/etag_caching/router.rb
index 53f3f442bc3..ca49eda51fb 100644
--- a/lib/gitlab/etag_caching/router.rb
+++ b/lib/gitlab/etag_caching/router.rb
@@ -10,10 +10,10 @@ module Gitlab
# - Ending in `issues/id`/realtime_changes` for the `issue_title` route
USED_IN_ROUTES = %w[noteable issue notes issues realtime_changes
commit pipelines merge_requests builds
- new].freeze
-
+ new environments].freeze
RESERVED_WORDS = Gitlab::PathRegex::ILLEGAL_PROJECT_PATH_WORDS - USED_IN_ROUTES
RESERVED_WORDS_REGEX = Regexp.union(*RESERVED_WORDS.map(&Regexp.method(:escape)))
+
ROUTES = [
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/noteable/issue/\d+/notes\z),
@@ -46,6 +46,10 @@ module Gitlab
Gitlab::EtagCaching::Router::Route.new(
%r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/builds/\d+\.json\z),
'project_build'
+ ),
+ Gitlab::EtagCaching::Router::Route.new(
+ %r(^(?!.*(#{RESERVED_WORDS_REGEX})).*/environments\.json\z),
+ 'environments'
)
].freeze
diff --git a/lib/gitlab/git/blame.rb b/lib/gitlab/git/blame.rb
index 58193391926..66829a03c2e 100644
--- a/lib/gitlab/git/blame.rb
+++ b/lib/gitlab/git/blame.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Blame
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_reader :lines, :blames
diff --git a/lib/gitlab/git/blob.rb b/lib/gitlab/git/blob.rb
index c1b31618e0d..d60e607b02b 100644
--- a/lib/gitlab/git/blob.rb
+++ b/lib/gitlab/git/blob.rb
@@ -2,7 +2,7 @@ module Gitlab
module Git
class Blob
include Linguist::BlobHelper
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# This number is the maximum amount of data that we want to display to
# the user. We load as much as we can for encoding detection
@@ -88,6 +88,7 @@ module Gitlab
new(
id: blob_entry[:oid],
name: blob_entry[:name],
+ size: 0,
data: '',
path: path,
commit_id: sha
diff --git a/lib/gitlab/git/commit.rb b/lib/gitlab/git/commit.rb
index 297531db4cc..bb04731f08c 100644
--- a/lib/gitlab/git/commit.rb
+++ b/lib/gitlab/git/commit.rb
@@ -2,7 +2,7 @@
module Gitlab
module Git
class Commit
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_accessor :raw_commit, :head, :refs
diff --git a/lib/gitlab/git/diff.rb b/lib/gitlab/git/diff.rb
index deade337354..0594ac8e213 100644
--- a/lib/gitlab/git/diff.rb
+++ b/lib/gitlab/git/diff.rb
@@ -3,7 +3,7 @@ module Gitlab
module Git
class Diff
TimeoutError = Class.new(StandardError)
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Diff properties
attr_accessor :old_path, :new_path, :a_mode, :b_mode, :diff
@@ -15,13 +15,16 @@ module Gitlab
alias_method :deleted_file?, :deleted_file
alias_method :renamed_file?, :renamed_file
+ attr_accessor :expanded
+
+ # We need this accessor because of `to_hash` and `init_from_hash`
attr_accessor :too_large
# The maximum size of a diff to display.
- DIFF_SIZE_LIMIT = 102400 # 100 KB
+ SIZE_LIMIT = 100.kilobytes
# The maximum size before a diff is collapsed.
- DIFF_COLLAPSE_LIMIT = 10240 # 10 KB
+ COLLAPSE_LIMIT = 10.kilobytes
class << self
def between(repo, head, base, options = {}, *paths)
@@ -152,7 +155,7 @@ module Gitlab
:include_untracked_content, :skip_binary_check,
:include_typechange, :include_typechange_trees,
:ignore_filemode, :recurse_ignored_dirs, :paths,
- :max_files, :max_lines, :all_diffs, :no_collapse]
+ :max_files, :max_lines, :limits, :expanded]
if default_options
actual_defaults = default_options.dup
@@ -177,16 +180,18 @@ module Gitlab
end
end
- def initialize(raw_diff, collapse: false)
+ def initialize(raw_diff, expanded: true)
+ @expanded = expanded
+
case raw_diff
when Hash
init_from_hash(raw_diff)
- prune_diff_if_eligible(collapse)
+ prune_diff_if_eligible
when Rugged::Patch, Rugged::Diff::Delta
- init_from_rugged(raw_diff, collapse: collapse)
+ init_from_rugged(raw_diff)
when Gitaly::CommitDiffResponse
init_from_gitaly(raw_diff)
- prune_diff_if_eligible(collapse)
+ prune_diff_if_eligible
when Gitaly::CommitDelta
init_from_gitaly(raw_diff)
when nil
@@ -226,17 +231,13 @@ module Gitlab
def too_large?
if @too_large.nil?
- @too_large = @diff.bytesize >= DIFF_SIZE_LIMIT
+ @too_large = @diff.bytesize >= SIZE_LIMIT
else
@too_large
end
end
- def collapsible?
- @diff.bytesize >= DIFF_COLLAPSE_LIMIT
- end
-
- def prune_large_diff!
+ def too_large!
@diff = ''
@line_count = 0
@too_large = true
@@ -244,10 +245,11 @@ module Gitlab
def collapsed?
return @collapsed if defined?(@collapsed)
- false
+
+ @collapsed = !expanded && @diff.bytesize >= COLLAPSE_LIMIT
end
- def prune_collapsed_diff!
+ def collapse!
@diff = ''
@line_count = 0
@collapsed = true
@@ -255,9 +257,9 @@ module Gitlab
private
- def init_from_rugged(rugged, collapse: false)
+ def init_from_rugged(rugged)
if rugged.is_a?(Rugged::Patch)
- init_from_rugged_patch(rugged, collapse: collapse)
+ init_from_rugged_patch(rugged)
d = rugged.delta
else
d = rugged
@@ -272,10 +274,10 @@ module Gitlab
@deleted_file = d.deleted?
end
- def init_from_rugged_patch(patch, collapse: false)
+ def init_from_rugged_patch(patch)
# Don't bother initializing diffs that are too large. If a diff is
# binary we're not going to display anything so we skip the size check.
- return if !patch.delta.binary? && prune_large_patch(patch, collapse)
+ return if !patch.delta.binary? && prune_large_patch(patch)
@diff = encode!(strip_diff_headers(patch.to_s))
end
@@ -299,29 +301,32 @@ module Gitlab
@deleted_file = msg.to_id == BLANK_SHA
end
- def prune_diff_if_eligible(collapse = false)
- prune_large_diff! if too_large?
- prune_collapsed_diff! if collapse && collapsible?
+ def prune_diff_if_eligible
+ if too_large?
+ too_large!
+ elsif collapsed?
+ collapse!
+ end
end
# If the patch surpasses any of the diff limits it calls the appropiate
# prune method and returns true. Otherwise returns false.
- def prune_large_patch(patch, collapse)
+ def prune_large_patch(patch)
size = 0
patch.each_hunk do |hunk|
hunk.each_line do |line|
size += line.content.bytesize
- if size >= DIFF_SIZE_LIMIT
- prune_large_diff!
+ if size >= SIZE_LIMIT
+ too_large!
return true
end
end
end
- if collapse && size >= DIFF_COLLAPSE_LIMIT
- prune_collapsed_diff!
+ if !expanded && size >= COLLAPSE_LIMIT
+ collapse!
return true
end
diff --git a/lib/gitlab/git/diff_collection.rb b/lib/gitlab/git/diff_collection.rb
index 898a5ae15f2..334e06a6eca 100644
--- a/lib/gitlab/git/diff_collection.rb
+++ b/lib/gitlab/git/diff_collection.rb
@@ -9,12 +9,12 @@ module Gitlab
@iterator = iterator
@max_files = options.fetch(:max_files, DEFAULT_LIMITS[:max_files])
@max_lines = options.fetch(:max_lines, DEFAULT_LIMITS[:max_lines])
- @max_bytes = @max_files * 5120 # Average 5 KB per file
+ @max_bytes = @max_files * 5.kilobytes # Average 5 KB per file
@safe_max_files = [@max_files, DEFAULT_LIMITS[:max_files]].min
@safe_max_lines = [@max_lines, DEFAULT_LIMITS[:max_lines]].min
- @safe_max_bytes = @safe_max_files * 5120 # Average 5 KB per file
- @all_diffs = !!options.fetch(:all_diffs, false)
- @no_collapse = !!options.fetch(:no_collapse, true)
+ @safe_max_bytes = @safe_max_files * 5.kilobytes # Average 5 KB per file
+ @enforce_limits = !!options.fetch(:limits, true)
+ @expanded = !!options.fetch(:expanded, true)
@line_count = 0
@byte_count = 0
@@ -88,23 +88,23 @@ module Gitlab
@iterator.each do |raw|
@empty = false
- if !@all_diffs && i >= @max_files
+ if @enforce_limits && i >= @max_files
@overflow = true
break
end
- collapse = !@all_diffs && !@no_collapse
+ expanded = !@enforce_limits || @expanded
- diff = Gitlab::Git::Diff.new(raw, collapse: collapse)
+ diff = Gitlab::Git::Diff.new(raw, expanded: expanded)
- if collapse && over_safe_limits?(i)
- diff.prune_collapsed_diff!
+ if !expanded && over_safe_limits?(i)
+ diff.collapse!
end
@line_count += diff.line_count
@byte_count += diff.diff.bytesize
- if !@all_diffs && (@line_count >= @max_lines || @byte_count >= @max_bytes)
+ if @enforce_limits && (@line_count >= @max_lines || @byte_count >= @max_bytes)
# This last Diff instance pushes us over the lines limit. We stop and
# discard it.
@overflow = true
diff --git a/lib/gitlab/git/encoding_helper.rb b/lib/gitlab/git/encoding_helper.rb
deleted file mode 100644
index f918074cb14..00000000000
--- a/lib/gitlab/git/encoding_helper.rb
+++ /dev/null
@@ -1,64 +0,0 @@
-module Gitlab
- module Git
- module EncodingHelper
- extend self
-
- # This threshold is carefully tweaked to prevent usage of encodings detected
- # by CharlockHolmes with low confidence. If CharlockHolmes confidence is low,
- # we're better off sticking with utf8 encoding.
- # Reason: git diff can return strings with invalid utf8 byte sequences if it
- # truncates a diff in the middle of a multibyte character. In this case
- # CharlockHolmes will try to guess the encoding and will likely suggest an
- # obscure encoding with low confidence.
- # There is a lot more info with this merge request:
- # https://gitlab.com/gitlab-org/gitlab_git/merge_requests/77#note_4754193
- ENCODING_CONFIDENCE_THRESHOLD = 40
-
- def encode!(message)
- return nil unless message.respond_to? :force_encoding
-
- # if message is utf-8 encoding, just return it
- message.force_encoding("UTF-8")
- return message if message.valid_encoding?
-
- # return message if message type is binary
- detect = CharlockHolmes::EncodingDetector.detect(message)
- return message.force_encoding("BINARY") if detect && detect[:type] == :binary
-
- # force detected encoding if we have sufficient confidence.
- if detect && detect[:encoding] && detect[:confidence] > ENCODING_CONFIDENCE_THRESHOLD
- message.force_encoding(detect[:encoding])
- end
-
- # encode and clean the bad chars
- message.replace clean(message)
- rescue
- encoding = detect ? detect[:encoding] : "unknown"
- "--broken encoding: #{encoding}"
- end
-
- def encode_utf8(message)
- detect = CharlockHolmes::EncodingDetector.detect(message)
- if detect
- begin
- CharlockHolmes::Converter.convert(message, detect[:encoding], 'UTF-8')
- rescue ArgumentError => e
- Rails.logger.warn("Ignoring error converting #{detect[:encoding]} into UTF8: #{e.message}")
-
- ''
- end
- else
- clean(message)
- end
- end
-
- private
-
- def clean(message)
- message.encode("UTF-16BE", undef: :replace, invalid: :replace, replace: "")
- .encode("UTF-8")
- .gsub("\0".encode("UTF-8"), "")
- end
- end
- end
-end
diff --git a/lib/gitlab/git/ref.rb b/lib/gitlab/git/ref.rb
index 37ef6836742..ebf7393dc61 100644
--- a/lib/gitlab/git/ref.rb
+++ b/lib/gitlab/git/ref.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Ref
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
# Branch or tag name
# without "refs/tags|heads" prefix
diff --git a/lib/gitlab/git/tree.rb b/lib/gitlab/git/tree.rb
index d41256d9a84..b9afa05c819 100644
--- a/lib/gitlab/git/tree.rb
+++ b/lib/gitlab/git/tree.rb
@@ -1,7 +1,7 @@
module Gitlab
module Git
class Tree
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
attr_accessor :id, :root_id, :name, :path, :type,
:mode, :commit_id, :submodule_url
diff --git a/lib/gitlab/utils.rb b/lib/gitlab/utils.rb
index 4c395b4266e..fa182c4deda 100644
--- a/lib/gitlab/utils.rb
+++ b/lib/gitlab/utils.rb
@@ -21,5 +21,13 @@ module Gitlab
nil
end
+
+ def boolean_to_yes_no(bool)
+ if bool
+ 'Yes'
+ else
+ 'No'
+ end
+ end
end
end
diff --git a/lib/system_check.rb b/lib/system_check.rb
new file mode 100644
index 00000000000..466c39904fa
--- /dev/null
+++ b/lib/system_check.rb
@@ -0,0 +1,21 @@
+# Library to perform System Checks
+#
+# Every Check is implemented as its own class inherited from SystemCheck::BaseCheck
+# Execution coordination and boilerplate output is done by the SystemCheck::SimpleExecutor
+#
+# This structure decouples checks from Rake tasks and facilitates unit-testing
+module SystemCheck
+ # Executes a bunch of checks for specified component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ # @param [Array<BaseCheck>] checks classes of corresponding checks to be executed in the same order
+ def self.run(component, checks = [])
+ executor = SimpleExecutor.new(component)
+
+ checks.each do |check|
+ executor << check
+ end
+
+ executor.execute
+ end
+end
diff --git a/lib/system_check/app/active_users_check.rb b/lib/system_check/app/active_users_check.rb
new file mode 100644
index 00000000000..1d72c8d6903
--- /dev/null
+++ b/lib/system_check/app/active_users_check.rb
@@ -0,0 +1,17 @@
+module SystemCheck
+ module App
+ class ActiveUsersCheck < SystemCheck::BaseCheck
+ set_name 'Active users:'
+
+ def multi_check
+ active_users = User.active.count
+
+ if active_users > 0
+ $stdout.puts active_users.to_s.color(:green)
+ else
+ $stdout.puts active_users.to_s.color(:red)
+ end
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/database_config_exists_check.rb b/lib/system_check/app/database_config_exists_check.rb
new file mode 100644
index 00000000000..d1fae192350
--- /dev/null
+++ b/lib/system_check/app/database_config_exists_check.rb
@@ -0,0 +1,25 @@
+module SystemCheck
+ module App
+ class DatabaseConfigExistsCheck < SystemCheck::BaseCheck
+ set_name 'Database config exists?'
+
+ def check?
+ database_config_file = Rails.root.join('config', 'database.yml')
+
+ File.exist?(database_config_file)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Copy config/database.yml.<your db> to config/database.yml',
+ 'Check that the information in config/database.yml is correct'
+ )
+ for_more_information(
+ 'doc/install/databases.md',
+ 'http://guides.rubyonrails.org/getting_started.html#configuring-a-database'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/git_config_check.rb b/lib/system_check/app/git_config_check.rb
new file mode 100644
index 00000000000..198867f7ac6
--- /dev/null
+++ b/lib/system_check/app/git_config_check.rb
@@ -0,0 +1,42 @@
+module SystemCheck
+ module App
+ class GitConfigCheck < SystemCheck::BaseCheck
+ OPTIONS = {
+ 'core.autocrlf' => 'input'
+ }.freeze
+
+ set_name 'Git configured correctly?'
+
+ def check?
+ correct_options = OPTIONS.map do |name, value|
+ run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
+ end
+
+ correct_options.all?
+ end
+
+ # Tries to configure git itself
+ #
+ # Returns true if all subcommands were successful (according to their exit code)
+ # Returns false if any or all subcommands failed.
+ def repair!
+ return false unless is_gitlab_user?
+
+ command_success = OPTIONS.map do |name, value|
+ system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
+ end
+
+ command_success.all?
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{OPTIONS['core.autocrlf']}\"")
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/git_version_check.rb b/lib/system_check/app/git_version_check.rb
new file mode 100644
index 00000000000..c388682dfb4
--- /dev/null
+++ b/lib/system_check/app/git_version_check.rb
@@ -0,0 +1,29 @@
+module SystemCheck
+ module App
+ class GitVersionCheck < SystemCheck::BaseCheck
+ set_name -> { "Git version >= #{self.required_version} ?" }
+ set_check_pass -> { "yes (#{self.current_version})" }
+
+ def self.required_version
+ @required_version ||= Gitlab::VersionInfo.new(2, 7, 3)
+ end
+
+ def self.current_version
+ @current_version ||= Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
+ end
+
+ def check?
+ self.class.current_version.valid? && self.class.required_version <= self.class.current_version
+ end
+
+ def show_error
+ $stdout.puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
+
+ try_fixing_it(
+ "Update your git to a version >= #{self.class.required_version} from #{self.class.current_version}"
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/gitlab_config_exists_check.rb b/lib/system_check/app/gitlab_config_exists_check.rb
new file mode 100644
index 00000000000..247aa0994e4
--- /dev/null
+++ b/lib/system_check/app/gitlab_config_exists_check.rb
@@ -0,0 +1,24 @@
+module SystemCheck
+ module App
+ class GitlabConfigExistsCheck < SystemCheck::BaseCheck
+ set_name 'GitLab config exists?'
+
+ def check?
+ gitlab_config_file = Rails.root.join('config', 'gitlab.yml')
+
+ File.exist?(gitlab_config_file)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Copy config/gitlab.yml.example to config/gitlab.yml',
+ 'Update config/gitlab.yml to match your setup'
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/gitlab_config_up_to_date_check.rb b/lib/system_check/app/gitlab_config_up_to_date_check.rb
new file mode 100644
index 00000000000..c609e48e133
--- /dev/null
+++ b/lib/system_check/app/gitlab_config_up_to_date_check.rb
@@ -0,0 +1,30 @@
+module SystemCheck
+ module App
+ class GitlabConfigUpToDateCheck < SystemCheck::BaseCheck
+ set_name 'GitLab config up to date?'
+ set_skip_reason "can't check because of previous errors"
+
+ def skip?
+ gitlab_config_file = Rails.root.join('config', 'gitlab.yml')
+ !File.exist?(gitlab_config_file)
+ end
+
+ def check?
+ # omniauth or ldap could have been deleted from the file
+ !Gitlab.config['git_host']
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Back-up your config/gitlab.yml',
+ 'Copy config/gitlab.yml.example to config/gitlab.yml',
+ 'Update config/gitlab.yml to match your setup'
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/init_script_exists_check.rb b/lib/system_check/app/init_script_exists_check.rb
new file mode 100644
index 00000000000..d246e058e86
--- /dev/null
+++ b/lib/system_check/app/init_script_exists_check.rb
@@ -0,0 +1,27 @@
+module SystemCheck
+ module App
+ class InitScriptExistsCheck < SystemCheck::BaseCheck
+ set_name 'Init script exists?'
+ set_skip_reason 'skipped (omnibus-gitlab has no init script)'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def check?
+ script_path = '/etc/init.d/gitlab'
+ File.exist?(script_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Install the init script'
+ )
+ for_more_information(
+ see_installation_guide_section 'Install Init Script'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/init_script_up_to_date_check.rb b/lib/system_check/app/init_script_up_to_date_check.rb
new file mode 100644
index 00000000000..015c7ed1731
--- /dev/null
+++ b/lib/system_check/app/init_script_up_to_date_check.rb
@@ -0,0 +1,43 @@
+module SystemCheck
+ module App
+ class InitScriptUpToDateCheck < SystemCheck::BaseCheck
+ SCRIPT_PATH = '/etc/init.d/gitlab'.freeze
+
+ set_name 'Init script up-to-date?'
+ set_skip_reason 'skipped (omnibus-gitlab has no init script)'
+
+ def skip?
+ omnibus_gitlab?
+ end
+
+ def multi_check
+ recipe_path = Rails.root.join('lib/support/init.d/', 'gitlab')
+
+ unless File.exist?(SCRIPT_PATH)
+ $stdout.puts "can't check because of previous errors".color(:magenta)
+ return
+ end
+
+ recipe_content = File.read(recipe_path)
+ script_content = File.read(SCRIPT_PATH)
+
+ if recipe_content == script_content
+ $stdout.puts 'yes'.color(:green)
+ else
+ $stdout.puts 'no'.color(:red)
+ show_error
+ end
+ end
+
+ def show_error
+ try_fixing_it(
+ 'Re-download the init script'
+ )
+ for_more_information(
+ see_installation_guide_section 'Install Init Script'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/log_writable_check.rb b/lib/system_check/app/log_writable_check.rb
new file mode 100644
index 00000000000..3e0c436d6ee
--- /dev/null
+++ b/lib/system_check/app/log_writable_check.rb
@@ -0,0 +1,28 @@
+module SystemCheck
+ module App
+ class LogWritableCheck < SystemCheck::BaseCheck
+ set_name 'Log directory writable?'
+
+ def check?
+ File.writable?(log_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R gitlab #{log_path}",
+ "sudo chmod -R u+rwX #{log_path}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def log_path
+ Rails.root.join('log')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/migrations_are_up_check.rb b/lib/system_check/app/migrations_are_up_check.rb
new file mode 100644
index 00000000000..5eedbacce77
--- /dev/null
+++ b/lib/system_check/app/migrations_are_up_check.rb
@@ -0,0 +1,20 @@
+module SystemCheck
+ module App
+ class MigrationsAreUpCheck < SystemCheck::BaseCheck
+ set_name 'All migrations up?'
+
+ def check?
+ migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
+
+ migration_status !~ /down\s+\d{14}/
+ end
+
+ def show_error
+ try_fixing_it(
+ sudo_gitlab('bundle exec rake db:migrate RAILS_ENV=production')
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/orphaned_group_members_check.rb b/lib/system_check/app/orphaned_group_members_check.rb
new file mode 100644
index 00000000000..2b46d36fe51
--- /dev/null
+++ b/lib/system_check/app/orphaned_group_members_check.rb
@@ -0,0 +1,20 @@
+module SystemCheck
+ module App
+ class OrphanedGroupMembersCheck < SystemCheck::BaseCheck
+ set_name 'Database contains orphaned GroupMembers?'
+ set_check_pass 'no'
+ set_check_fail 'yes'
+
+ def check?
+ !GroupMember.where('user_id not in (select id from users)').exists?
+ end
+
+ def show_error
+ try_fixing_it(
+ 'You can delete the orphaned records using something along the lines of:',
+ sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
+ )
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/projects_have_namespace_check.rb b/lib/system_check/app/projects_have_namespace_check.rb
new file mode 100644
index 00000000000..a6ec9f7665c
--- /dev/null
+++ b/lib/system_check/app/projects_have_namespace_check.rb
@@ -0,0 +1,37 @@
+module SystemCheck
+ module App
+ class ProjectsHaveNamespaceCheck < SystemCheck::BaseCheck
+ set_name 'Projects have namespace:'
+ set_skip_reason "can't check, you have no projects"
+
+ def skip?
+ !Project.exists?
+ end
+
+ def multi_check
+ $stdout.puts ''
+
+ Project.find_each(batch_size: 100) do |project|
+ $stdout.print sanitized_message(project)
+
+ if project.namespace
+ $stdout.puts 'yes'.color(:green)
+ else
+ $stdout.puts 'no'.color(:red)
+ show_error
+ end
+ end
+ end
+
+ def show_error
+ try_fixing_it(
+ "Migrate global projects"
+ )
+ for_more_information(
+ "doc/update/5.4-to-6.0.md in section \"#global-projects\""
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/redis_version_check.rb b/lib/system_check/app/redis_version_check.rb
new file mode 100644
index 00000000000..a0610e73576
--- /dev/null
+++ b/lib/system_check/app/redis_version_check.rb
@@ -0,0 +1,25 @@
+module SystemCheck
+ module App
+ class RedisVersionCheck < SystemCheck::BaseCheck
+ MIN_REDIS_VERSION = '2.8.0'.freeze
+ set_name "Redis version >= #{MIN_REDIS_VERSION}?"
+
+ def check?
+ redis_version = run_command(%w(redis-cli --version))
+ redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
+
+ redis_version && (Gem::Version.new(redis_version[1]) > Gem::Version.new(MIN_REDIS_VERSION))
+ end
+
+ def show_error
+ try_fixing_it(
+ "Update your redis server to a version >= #{MIN_REDIS_VERSION}"
+ )
+ for_more_information(
+ 'gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/ruby_version_check.rb b/lib/system_check/app/ruby_version_check.rb
new file mode 100644
index 00000000000..fd82f5f8a4a
--- /dev/null
+++ b/lib/system_check/app/ruby_version_check.rb
@@ -0,0 +1,27 @@
+module SystemCheck
+ module App
+ class RubyVersionCheck < SystemCheck::BaseCheck
+ set_name -> { "Ruby version >= #{self.required_version} ?" }
+ set_check_pass -> { "yes (#{self.current_version})" }
+
+ def self.required_version
+ @required_version ||= Gitlab::VersionInfo.new(2, 3, 3)
+ end
+
+ def self.current_version
+ @current_version ||= Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
+ end
+
+ def check?
+ self.class.current_version.valid? && self.class.required_version <= self.class.current_version
+ end
+
+ def show_error
+ try_fixing_it(
+ "Update your ruby to a version >= #{self.class.required_version} from #{self.class.current_version}"
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/tmp_writable_check.rb b/lib/system_check/app/tmp_writable_check.rb
new file mode 100644
index 00000000000..99a75e57abf
--- /dev/null
+++ b/lib/system_check/app/tmp_writable_check.rb
@@ -0,0 +1,28 @@
+module SystemCheck
+ module App
+ class TmpWritableCheck < SystemCheck::BaseCheck
+ set_name 'Tmp directory writable?'
+
+ def check?
+ File.writable?(tmp_path)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R gitlab #{tmp_path}",
+ "sudo chmod -R u+rwX #{tmp_path}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def tmp_path
+ Rails.root.join('tmp')
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_directory_exists_check.rb b/lib/system_check/app/uploads_directory_exists_check.rb
new file mode 100644
index 00000000000..7026d0ba075
--- /dev/null
+++ b/lib/system_check/app/uploads_directory_exists_check.rb
@@ -0,0 +1,21 @@
+module SystemCheck
+ module App
+ class UploadsDirectoryExistsCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory exists?'
+
+ def check?
+ File.directory?(Rails.root.join('public/uploads'))
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_path_permission_check.rb b/lib/system_check/app/uploads_path_permission_check.rb
new file mode 100644
index 00000000000..7df6c060254
--- /dev/null
+++ b/lib/system_check/app/uploads_path_permission_check.rb
@@ -0,0 +1,36 @@
+module SystemCheck
+ module App
+ class UploadsPathPermissionCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory has correct permissions?'
+ set_skip_reason 'skipped (no uploads folder found)'
+
+ def skip?
+ !File.directory?(rails_uploads_path)
+ end
+
+ def check?
+ File.stat(uploads_fullpath).mode == 040700
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chmod 700 #{uploads_fullpath}"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def rails_uploads_path
+ Rails.root.join('public/uploads')
+ end
+
+ def uploads_fullpath
+ File.realpath(rails_uploads_path)
+ end
+ end
+ end
+end
diff --git a/lib/system_check/app/uploads_path_tmp_permission_check.rb b/lib/system_check/app/uploads_path_tmp_permission_check.rb
new file mode 100644
index 00000000000..b276a81eac1
--- /dev/null
+++ b/lib/system_check/app/uploads_path_tmp_permission_check.rb
@@ -0,0 +1,40 @@
+module SystemCheck
+ module App
+ class UploadsPathTmpPermissionCheck < SystemCheck::BaseCheck
+ set_name 'Uploads directory tmp has correct permissions?'
+ set_skip_reason 'skipped (no tmp uploads folder yet)'
+
+ def skip?
+ !File.directory?(uploads_fullpath) || !Dir.exist?(upload_path_tmp)
+ end
+
+ def check?
+ # If tmp upload dir has incorrect permissions, assume others do as well
+ # Verify drwx------ permissions
+ File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
+ end
+
+ def show_error
+ try_fixing_it(
+ "sudo chown -R #{gitlab_user} #{uploads_fullpath}",
+ "sudo find #{uploads_fullpath} -type f -exec chmod 0644 {} \\;",
+ "sudo find #{uploads_fullpath} -type d -not -path #{uploads_fullpath} -exec chmod 0700 {} \\;"
+ )
+ for_more_information(
+ see_installation_guide_section 'GitLab'
+ )
+ fix_and_rerun
+ end
+
+ private
+
+ def upload_path_tmp
+ File.join(uploads_fullpath, 'tmp')
+ end
+
+ def uploads_fullpath
+ File.realpath(Rails.root.join('public/uploads'))
+ end
+ end
+ end
+end
diff --git a/lib/system_check/base_check.rb b/lib/system_check/base_check.rb
new file mode 100644
index 00000000000..5dcb3f0886b
--- /dev/null
+++ b/lib/system_check/base_check.rb
@@ -0,0 +1,129 @@
+module SystemCheck
+ # Base class for Checks. You must inherit from here
+ # and implement the methods below when necessary
+ class BaseCheck
+ include ::SystemCheck::Helpers
+
+ # Define a custom term for when check passed
+ #
+ # @param [String] term used when check passed (default: 'yes')
+ def self.set_check_pass(term)
+ @check_pass = term
+ end
+
+ # Define a custom term for when check failed
+ #
+ # @param [String] term used when check failed (default: 'no')
+ def self.set_check_fail(term)
+ @check_fail = term
+ end
+
+ # Define the name of the SystemCheck that will be displayed during execution
+ #
+ # @param [String] name of the check
+ def self.set_name(name)
+ @name = name
+ end
+
+ # Define the reason why we skipped the SystemCheck
+ #
+ # This is only used if subclass implements `#skip?`
+ #
+ # @param [String] reason to be displayed
+ def self.set_skip_reason(reason)
+ @skip_reason = reason
+ end
+
+ # Term to be displayed when check passed
+ #
+ # @return [String] term when check passed ('yes' if not re-defined in a subclass)
+ def self.check_pass
+ call_or_return(@check_pass) || 'yes'
+ end
+
+ ## Term to be displayed when check failed
+ #
+ # @return [String] term when check failed ('no' if not re-defined in a subclass)
+ def self.check_fail
+ call_or_return(@check_fail) || 'no'
+ end
+
+ # Name of the SystemCheck defined by the subclass
+ #
+ # @return [String] the name
+ def self.display_name
+ call_or_return(@name) || self.name
+ end
+
+ # Skip reason defined by the subclass
+ #
+ # @return [String] the reason
+ def self.skip_reason
+ call_or_return(@skip_reason) || 'skipped'
+ end
+
+ # Does the check support automatically repair routine?
+ #
+ # @return [Boolean] whether check implemented `#repair!` method or not
+ def can_repair?
+ self.class.instance_methods(false).include?(:repair!)
+ end
+
+ def can_skip?
+ self.class.instance_methods(false).include?(:skip?)
+ end
+
+ def is_multi_check?
+ self.class.instance_methods(false).include?(:multi_check)
+ end
+
+ # Execute the check routine
+ #
+ # This is where you should implement the main logic that will return
+ # a boolean at the end
+ #
+ # You should not print any output to STDOUT here, use the specific methods instead
+ #
+ # @return [Boolean] whether check passed or failed
+ def check?
+ raise NotImplementedError
+ end
+
+ # Execute a custom check that cover multiple unities
+ #
+ # When using multi_check you have to provide the output yourself
+ def multi_check
+ raise NotImplementedError
+ end
+
+ # Prints troubleshooting instructions
+ #
+ # This is where you should print detailed information for any error found during #check?
+ #
+ # You may use helper methods to help format the output:
+ #
+ # @see #try_fixing_it
+ # @see #fix_and_rerun
+ # @see #for_more_infromation
+ def show_error
+ raise NotImplementedError
+ end
+
+ # When implemented by a subclass, will attempt to fix the issue automatically
+ def repair!
+ raise NotImplementedError
+ end
+
+ # When implemented by a subclass, will evaluate whether check should be skipped or not
+ #
+ # @return [Boolean] whether or not this check should be skipped
+ def skip?
+ raise NotImplementedError
+ end
+
+ def self.call_or_return(input)
+ input.respond_to?(:call) ? input.call : input
+ end
+ private_class_method :call_or_return
+ end
+end
diff --git a/lib/system_check/helpers.rb b/lib/system_check/helpers.rb
new file mode 100644
index 00000000000..c42ae4fe4c4
--- /dev/null
+++ b/lib/system_check/helpers.rb
@@ -0,0 +1,75 @@
+require 'tasks/gitlab/task_helpers'
+
+module SystemCheck
+ module Helpers
+ include ::Gitlab::TaskHelpers
+
+ # Display a message telling to fix and rerun the checks
+ def fix_and_rerun
+ $stdout.puts ' Please fix the error above and rerun the checks.'.color(:red)
+ end
+
+ # Display a formatted list of references (documentation or links) where to find more information
+ #
+ # @param [Array<String>] sources one or more references (documentation or links)
+ def for_more_information(*sources)
+ $stdout.puts ' For more information see:'.color(:blue)
+ sources.each do |source|
+ $stdout.puts " #{source}"
+ end
+ end
+
+ def see_installation_guide_section(section)
+ "doc/install/installation.md in section \"#{section}\""
+ end
+
+ # @deprecated This will no longer be used when all checks were executed using SystemCheck
+ def finished_checking(component)
+ $stdout.puts ''
+ $stdout.puts "Checking #{component.color(:yellow)} ... #{'Finished'.color(:green)}"
+ $stdout.puts ''
+ end
+
+ # @deprecated This will no longer be used when all checks were executed using SystemCheck
+ def start_checking(component)
+ $stdout.puts "Checking #{component.color(:yellow)} ..."
+ $stdout.puts ''
+ end
+
+ # Display a formatted list of instructions on how to fix the issue identified by the #check?
+ #
+ # @param [Array<String>] steps one or short sentences with help how to fix the issue
+ def try_fixing_it(*steps)
+ steps = steps.shift if steps.first.is_a?(Array)
+
+ $stdout.puts ' Try fixing it:'.color(:blue)
+ steps.each do |step|
+ $stdout.puts " #{step}"
+ end
+ end
+
+ def sanitized_message(project)
+ if should_sanitize?
+ "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
+ else
+ "#{project.name_with_namespace.color(:yellow)} ... "
+ end
+ end
+
+ def should_sanitize?
+ if ENV['SANITIZE'] == 'true'
+ true
+ else
+ false
+ end
+ end
+
+ def omnibus_gitlab?
+ Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails'
+ end
+
+ def sudo_gitlab(command)
+ "sudo -u #{gitlab_user} -H #{command}"
+ end
+ end
+end
diff --git a/lib/system_check/simple_executor.rb b/lib/system_check/simple_executor.rb
new file mode 100644
index 00000000000..dc2d4643a01
--- /dev/null
+++ b/lib/system_check/simple_executor.rb
@@ -0,0 +1,99 @@
+module SystemCheck
+ # Simple Executor is current default executor for GitLab
+ # It is a simple port from display logic in the old check.rake
+ #
+ # There is no concurrency level and the output is progressively
+ # printed into the STDOUT
+ #
+ # @attr_reader [Array<BaseCheck>] checks classes of corresponding checks to be executed in the same order
+ # @attr_reader [String] component name of the component relative to the checks being executed
+ class SimpleExecutor
+ attr_reader :checks
+ attr_reader :component
+
+ # @param [String] component name of the component relative to the checks being executed
+ def initialize(component)
+ raise ArgumentError unless component.is_a? String
+
+ @component = component
+ @checks = Set.new
+ end
+
+ # Add a check to be executed
+ #
+ # @param [BaseCheck] check class
+ def <<(check)
+ raise ArgumentError unless check < BaseCheck
+ @checks << check
+ end
+
+ # Executes defined checks in the specified order and outputs confirmation or error information
+ def execute
+ start_checking(component)
+
+ @checks.each do |check|
+ run_check(check)
+ end
+
+ finished_checking(component)
+ end
+
+ # Executes a single check
+ #
+ # @param [SystemCheck::BaseCheck] check_klass
+ def run_check(check_klass)
+ $stdout.print "#{check_klass.display_name} ... "
+
+ check = check_klass.new
+
+ # When implements skip method, we run it first, and if true, skip the check
+ if check.can_skip? && check.skip?
+ $stdout.puts check_klass.skip_reason.color(:magenta)
+ return
+ end
+
+ # When implements a multi check, we don't control the output
+ if check.is_multi_check?
+ check.multi_check
+ return
+ end
+
+ if check.check?
+ $stdout.puts check_klass.check_pass.color(:green)
+ else
+ $stdout.puts check_klass.check_fail.color(:red)
+
+ if check.can_repair?
+ $stdout.print 'Trying to fix error automatically. ...'
+ if check.repair!
+ $stdout.puts 'Success'.color(:green)
+ return
+ else
+ $stdout.puts 'Failed'.color(:red)
+ end
+ end
+
+ check.show_error
+ end
+ end
+
+ private
+
+ # Prints header content for the series of checks to be executed for this component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ def start_checking(component)
+ $stdout.puts "Checking #{component.color(:yellow)} ..."
+ $stdout.puts ''
+ end
+
+ # Prints footer content for the series of checks executed for this component
+ #
+ # @param [String] component name of the component relative to the checks being executed
+ def finished_checking(component)
+ $stdout.puts ''
+ $stdout.puts "Checking #{component.color(:yellow)} ... #{'Finished'.color(:green)}"
+ $stdout.puts ''
+ end
+ end
+end
diff --git a/lib/tasks/gettext.rake b/lib/tasks/gettext.rake
index 0aa21a4bd13..b27f7475115 100644
--- a/lib/tasks/gettext.rake
+++ b/lib/tasks/gettext.rake
@@ -11,4 +11,12 @@ namespace :gettext do
"{#{folders}}/**/*.{#{exts}}"
)
end
+
+ task :compile do
+ # See: https://gitlab.com/gitlab-org/gitlab-ce/issues/33014#note_31218998
+ FileUtils.touch(File.join(Rails.root, 'locale/gitlab.pot'))
+
+ Rake::Task['gettext:pack'].invoke
+ Rake::Task['gettext:po_to_json'].invoke
+ end
end
diff --git a/lib/tasks/gitlab/check.rake b/lib/tasks/gitlab/check.rake
index f41c73154f5..63c5e9b9c83 100644
--- a/lib/tasks/gitlab/check.rake
+++ b/lib/tasks/gitlab/check.rake
@@ -1,5 +1,9 @@
+# Temporary hack, until we migrate all checks to SystemCheck format
+require 'system_check'
+require 'system_check/helpers'
+
namespace :gitlab do
- desc "GitLab | Check the configuration of GitLab and its environment"
+ desc 'GitLab | Check the configuration of GitLab and its environment'
task check: %w{gitlab:gitlab_shell:check
gitlab:sidekiq:check
gitlab:incoming_email:check
@@ -7,331 +11,38 @@ namespace :gitlab do
gitlab:app:check}
namespace :app do
- desc "GitLab | Check the configuration of the GitLab Rails app"
+ desc 'GitLab | Check the configuration of the GitLab Rails app'
task check: :environment do
warn_user_is_not_gitlab
- start_checking "GitLab"
-
- check_git_config
- check_database_config_exists
- check_migrations_are_up
- check_orphaned_group_members
- check_gitlab_config_exists
- check_gitlab_config_not_outdated
- check_log_writable
- check_tmp_writable
- check_uploads
- check_init_script_exists
- check_init_script_up_to_date
- check_projects_have_namespace
- check_redis_version
- check_ruby_version
- check_git_version
- check_active_users
-
- finished_checking "GitLab"
- end
-
- # Checks
- ########################
-
- def check_git_config
- print "Git configured with autocrlf=input? ... "
-
- options = {
- "core.autocrlf" => "input"
- }
-
- correct_options = options.map do |name, value|
- run_command(%W(#{Gitlab.config.git.bin_path} config --global --get #{name})).try(:squish) == value
- end
-
- if correct_options.all?
- puts "yes".color(:green)
- else
- print "Trying to fix Git error automatically. ..."
-
- if auto_fix_git_config(options)
- puts "Success".color(:green)
- else
- puts "Failed".color(:red)
- try_fixing_it(
- sudo_gitlab("\"#{Gitlab.config.git.bin_path}\" config --global core.autocrlf \"#{options["core.autocrlf"]}\"")
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- end
- end
- end
-
- def check_database_config_exists
- print "Database config exists? ... "
-
- database_config_file = Rails.root.join("config", "database.yml")
-
- if File.exist?(database_config_file)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Copy config/database.yml.<your db> to config/database.yml",
- "Check that the information in config/database.yml is correct"
- )
- for_more_information(
- see_database_guide,
- "http://guides.rubyonrails.org/getting_started.html#configuring-a-database"
- )
- fix_and_rerun
- end
- end
-
- def check_gitlab_config_exists
- print "GitLab config exists? ... "
-
- gitlab_config_file = Rails.root.join("config", "gitlab.yml")
-
- if File.exist?(gitlab_config_file)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Copy config/gitlab.yml.example to config/gitlab.yml",
- "Update config/gitlab.yml to match your setup"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_gitlab_config_not_outdated
- print "GitLab config outdated? ... "
-
- gitlab_config_file = Rails.root.join("config", "gitlab.yml")
- unless File.exist?(gitlab_config_file)
- puts "can't check because of previous errors".color(:magenta)
- end
-
- # omniauth or ldap could have been deleted from the file
- unless Gitlab.config['git_host']
- puts "no".color(:green)
- else
- puts "yes".color(:red)
- try_fixing_it(
- "Backup your config/gitlab.yml",
- "Copy config/gitlab.yml.example to config/gitlab.yml",
- "Update config/gitlab.yml to match your setup"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_init_script_exists
- print "Init script exists? ... "
-
- if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
- return
- end
-
- script_path = "/etc/init.d/gitlab"
-
- if File.exist?(script_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Install the init script"
- )
- for_more_information(
- see_installation_guide_section "Install Init Script"
- )
- fix_and_rerun
- end
- end
-
- def check_init_script_up_to_date
- print "Init script up-to-date? ... "
-
- if omnibus_gitlab?
- puts 'skipped (omnibus-gitlab has no init script)'.color(:magenta)
- return
- end
-
- recipe_path = Rails.root.join("lib/support/init.d/", "gitlab")
- script_path = "/etc/init.d/gitlab"
-
- unless File.exist?(script_path)
- puts "can't check because of previous errors".color(:magenta)
- return
- end
-
- recipe_content = File.read(recipe_path)
- script_content = File.read(script_path)
-
- if recipe_content == script_content
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Redownload the init script"
- )
- for_more_information(
- see_installation_guide_section "Install Init Script"
- )
- fix_and_rerun
- end
- end
-
- def check_migrations_are_up
- print "All migrations up? ... "
-
- migration_status, _ = Gitlab::Popen.popen(%w(bundle exec rake db:migrate:status))
-
- unless migration_status =~ /down\s+\d{14}/
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- sudo_gitlab("bundle exec rake db:migrate RAILS_ENV=production")
- )
- fix_and_rerun
- end
- end
-
- def check_orphaned_group_members
- print "Database contains orphaned GroupMembers? ... "
- if GroupMember.where("user_id not in (select id from users)").count > 0
- puts "yes".color(:red)
- try_fixing_it(
- "You can delete the orphaned records using something along the lines of:",
- sudo_gitlab("bundle exec rails runner -e production 'GroupMember.where(\"user_id NOT IN (SELECT id FROM users)\").delete_all'")
- )
- else
- puts "no".color(:green)
- end
- end
-
- def check_log_writable
- print "Log directory writable? ... "
-
- log_path = Rails.root.join("log")
-
- if File.writable?(log_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R gitlab #{log_path}",
- "sudo chmod -R u+rwX #{log_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
- def check_tmp_writable
- print "Tmp directory writable? ... "
-
- tmp_path = Rails.root.join("tmp")
-
- if File.writable?(tmp_path)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R gitlab #{tmp_path}",
- "sudo chmod -R u+rwX #{tmp_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_uploads
- print "Uploads directory setup correctly? ... "
-
- unless File.directory?(Rails.root.join('public/uploads'))
- puts "no".color(:red)
- try_fixing_it(
- "sudo -u #{gitlab_user} mkdir #{Rails.root}/public/uploads"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- return
- end
-
- upload_path = File.realpath(Rails.root.join('public/uploads'))
- upload_path_tmp = File.join(upload_path, 'tmp')
-
- if File.stat(upload_path).mode == 040700
- unless Dir.exist?(upload_path_tmp)
- puts 'skipped (no tmp uploads folder yet)'.color(:magenta)
- return
- end
-
- # If tmp upload dir has incorrect permissions, assume others do as well
- # Verify drwx------ permissions
- if File.stat(upload_path_tmp).mode == 040700 && File.owned?(upload_path_tmp)
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chown -R #{gitlab_user} #{upload_path}",
- "sudo find #{upload_path} -type f -exec chmod 0644 {} \\;",
- "sudo find #{upload_path} -type d -not -path #{upload_path} -exec chmod 0700 {} \\;"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- else
- puts "no".color(:red)
- try_fixing_it(
- "sudo chmod 700 #{upload_path}"
- )
- for_more_information(
- see_installation_guide_section "GitLab"
- )
- fix_and_rerun
- end
- end
-
- def check_redis_version
- min_redis_version = "2.8.0"
- print "Redis version >= #{min_redis_version}? ... "
-
- redis_version = run_command(%w(redis-cli --version))
- redis_version = redis_version.try(:match, /redis-cli (\d+\.\d+\.\d+)/)
- if redis_version &&
- (Gem::Version.new(redis_version[1]) > Gem::Version.new(min_redis_version))
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your redis server to a version >= #{min_redis_version}"
- )
- for_more_information(
- "gitlab-public-wiki/wiki/Trouble-Shooting-Guide in section sidekiq"
- )
- fix_and_rerun
- end
+ checks = [
+ SystemCheck::App::GitConfigCheck,
+ SystemCheck::App::DatabaseConfigExistsCheck,
+ SystemCheck::App::MigrationsAreUpCheck,
+ SystemCheck::App::OrphanedGroupMembersCheck,
+ SystemCheck::App::GitlabConfigExistsCheck,
+ SystemCheck::App::GitlabConfigUpToDateCheck,
+ SystemCheck::App::LogWritableCheck,
+ SystemCheck::App::TmpWritableCheck,
+ SystemCheck::App::UploadsDirectoryExistsCheck,
+ SystemCheck::App::UploadsPathPermissionCheck,
+ SystemCheck::App::UploadsPathTmpPermissionCheck,
+ SystemCheck::App::InitScriptExistsCheck,
+ SystemCheck::App::InitScriptUpToDateCheck,
+ SystemCheck::App::ProjectsHaveNamespaceCheck,
+ SystemCheck::App::RedisVersionCheck,
+ SystemCheck::App::RubyVersionCheck,
+ SystemCheck::App::GitVersionCheck,
+ SystemCheck::App::ActiveUsersCheck
+ ]
+
+ SystemCheck.run('GitLab', checks)
end
end
namespace :gitlab_shell do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of GitLab Shell"
task check: :environment do
warn_user_is_not_gitlab
@@ -513,33 +224,6 @@ namespace :gitlab do
end
end
- def check_projects_have_namespace
- print "projects have namespace: ... "
-
- unless Project.count > 0
- puts "can't check, you have no projects".color(:magenta)
- return
- end
- puts ""
-
- Project.find_each(batch_size: 100) do |project|
- print sanitized_message(project)
-
- if project.namespace
- puts "yes".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Migrate global projects"
- )
- for_more_information(
- "doc/update/5.4-to-6.0.md in section \"#global-projects\""
- )
- fix_and_rerun
- end
- end
- end
-
# Helper methods
########################
@@ -565,6 +249,8 @@ namespace :gitlab do
end
namespace :sidekiq do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of Sidekiq"
task check: :environment do
warn_user_is_not_gitlab
@@ -623,6 +309,8 @@ namespace :gitlab do
end
namespace :incoming_email do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the configuration of Reply by email"
task check: :environment do
warn_user_is_not_gitlab
@@ -757,6 +445,8 @@ namespace :gitlab do
end
namespace :ldap do
+ include SystemCheck::Helpers
+
task :check, [:limit] => :environment do |_, args|
# Only show up to 100 results because LDAP directories can be very big.
# This setting only affects the `rake gitlab:check` script.
@@ -812,6 +502,8 @@ namespace :gitlab do
end
namespace :repo do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the integrity of the repositories managed by GitLab"
task check: :environment do
Gitlab.config.repositories.storages.each do |name, repository_storage|
@@ -826,6 +518,8 @@ namespace :gitlab do
end
namespace :user do
+ include SystemCheck::Helpers
+
desc "GitLab | Check the integrity of a specific user's repositories"
task :check_repos, [:username] => :environment do |t, args|
username = args[:username] || prompt("Check repository integrity for fsername? ".color(:blue))
@@ -848,55 +542,6 @@ namespace :gitlab do
# Helper methods
##########################
- def fix_and_rerun
- puts " Please fix the error above and rerun the checks.".color(:red)
- end
-
- def for_more_information(*sources)
- sources = sources.shift if sources.first.is_a?(Array)
-
- puts " For more information see:".color(:blue)
- sources.each do |source|
- puts " #{source}"
- end
- end
-
- def finished_checking(component)
- puts ""
- puts "Checking #{component.color(:yellow)} ... #{"Finished".color(:green)}"
- puts ""
- end
-
- def see_database_guide
- "doc/install/databases.md"
- end
-
- def see_installation_guide_section(section)
- "doc/install/installation.md in section \"#{section}\""
- end
-
- def sudo_gitlab(command)
- "sudo -u #{gitlab_user} -H #{command}"
- end
-
- def gitlab_user
- Gitlab.config.gitlab.user
- end
-
- def start_checking(component)
- puts "Checking #{component.color(:yellow)} ..."
- puts ""
- end
-
- def try_fixing_it(*steps)
- steps = steps.shift if steps.first.is_a?(Array)
-
- puts " Try fixing it:".color(:blue)
- steps.each do |step|
- puts " #{step}"
- end
- end
-
def check_gitlab_shell
required_version = Gitlab::VersionInfo.new(gitlab_shell_major_version, gitlab_shell_minor_version, gitlab_shell_patch_version)
current_version = Gitlab::VersionInfo.parse(gitlab_shell_version)
@@ -909,65 +554,6 @@ namespace :gitlab do
end
end
- def check_ruby_version
- required_version = Gitlab::VersionInfo.new(2, 1, 0)
- current_version = Gitlab::VersionInfo.parse(run_command(%w(ruby --version)))
-
- print "Ruby version >= #{required_version} ? ... "
-
- if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your ruby to a version >= #{required_version} from #{current_version}"
- )
- fix_and_rerun
- end
- end
-
- def check_git_version
- required_version = Gitlab::VersionInfo.new(2, 7, 3)
- current_version = Gitlab::VersionInfo.parse(run_command(%W(#{Gitlab.config.git.bin_path} --version)))
-
- puts "Your git bin path is \"#{Gitlab.config.git.bin_path}\""
- print "Git version >= #{required_version} ? ... "
-
- if current_version.valid? && required_version <= current_version
- puts "yes (#{current_version})".color(:green)
- else
- puts "no".color(:red)
- try_fixing_it(
- "Update your git to a version >= #{required_version} from #{current_version}"
- )
- fix_and_rerun
- end
- end
-
- def check_active_users
- puts "Active users: #{User.active.count}"
- end
-
- def omnibus_gitlab?
- Dir.pwd == '/opt/gitlab/embedded/service/gitlab-rails'
- end
-
- def sanitized_message(project)
- if should_sanitize?
- "#{project.namespace_id.to_s.color(:yellow)}/#{project.id.to_s.color(:yellow)} ... "
- else
- "#{project.name_with_namespace.color(:yellow)} ... "
- end
- end
-
- def should_sanitize?
- if ENV['SANITIZE'] == "true"
- true
- else
- false
- end
- end
-
def check_repo_integrity(repo_dir)
puts "\nChecking repo at #{repo_dir.color(:yellow)}"
diff --git a/lib/tasks/gitlab/task_helpers.rb b/lib/tasks/gitlab/task_helpers.rb
index e3c9d3b491c..964aa0fe1bc 100644
--- a/lib/tasks/gitlab/task_helpers.rb
+++ b/lib/tasks/gitlab/task_helpers.rb
@@ -98,34 +98,30 @@ module Gitlab
end
end
+ def gitlab_user
+ Gitlab.config.gitlab.user
+ end
+
+ def is_gitlab_user?
+ return @is_gitlab_user unless @is_gitlab_user.nil?
+
+ current_user = run_command(%w(whoami)).chomp
+ @is_gitlab_user = current_user == gitlab_user
+ end
+
def warn_user_is_not_gitlab
- unless @warned_user_not_gitlab
- gitlab_user = Gitlab.config.gitlab.user
+ return if @warned_user_not_gitlab
+
+ unless is_gitlab_user?
current_user = run_command(%w(whoami)).chomp
- unless current_user == gitlab_user
- puts " Warning ".color(:black).background(:yellow)
- puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
- puts " Things may work\/fail for the wrong reasons."
- puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
- puts ""
- end
- @warned_user_not_gitlab = true
- end
- end
- # Tries to configure git itself
- #
- # Returns true if all subcommands were successfull (according to their exit code)
- # Returns false if any or all subcommands failed.
- def auto_fix_git_config(options)
- if !@warned_user_not_gitlab
- command_success = options.map do |name, value|
- system(*%W(#{Gitlab.config.git.bin_path} config --global #{name} #{value}))
- end
+ puts " Warning ".color(:black).background(:yellow)
+ puts " You are running as user #{current_user.color(:magenta)}, we hope you know what you are doing."
+ puts " Things may work\/fail for the wrong reasons."
+ puts " For correct results you should run this as user #{gitlab_user.color(:magenta)}."
+ puts ""
- command_success.all?
- else
- false
+ @warned_user_not_gitlab = true
end
end
diff --git a/rubocop/cop/activerecord_serialize.rb b/rubocop/cop/activerecord_serialize.rb
new file mode 100644
index 00000000000..bfa0cff9a67
--- /dev/null
+++ b/rubocop/cop/activerecord_serialize.rb
@@ -0,0 +1,24 @@
+module RuboCop
+ module Cop
+ # Cop that prevents the use of `serialize` in ActiveRecord models.
+ class ActiverecordSerialize < RuboCop::Cop::Cop
+ MSG = 'Do not store serialized data in the database, use separate columns and/or tables instead'.freeze
+
+ def on_send(node)
+ return unless in_models?(node)
+
+ add_offense(node, :selector) if node.children[1] == :serialize
+ end
+
+ def models_path
+ File.join(Dir.pwd, 'app', 'models')
+ end
+
+ def in_models?(node)
+ path = node.location.expression.source_buffer.name
+
+ path.start_with?(models_path)
+ end
+ end
+ end
+end
diff --git a/rubocop/rubocop.rb b/rubocop/rubocop.rb
index b65efbc41f4..17d2bf6aa1c 100644
--- a/rubocop/rubocop.rb
+++ b/rubocop/rubocop.rb
@@ -1,5 +1,6 @@
require_relative 'cop/custom_error_class'
require_relative 'cop/gem_fetcher'
+require_relative 'cop/activerecord_serialize'
require_relative 'cop/migration/add_column'
require_relative 'cop/migration/add_column_with_default_to_large_table'
require_relative 'cop/migration/add_concurrent_foreign_key'
diff --git a/spec/controllers/projects/environments_controller_spec.rb b/spec/controllers/projects/environments_controller_spec.rb
index de13f17012b..f6840578145 100644
--- a/spec/controllers/projects/environments_controller_spec.rb
+++ b/spec/controllers/projects/environments_controller_spec.rb
@@ -57,6 +57,11 @@ describe Projects::EnvironmentsController do
expect(json_response['available_count']).to eq 3
expect(json_response['stopped_count']).to eq 1
end
+
+ it 'sets the polling interval header' do
+ expect(response).to have_http_status(:ok)
+ expect(response.headers['Poll-Interval']).to eq("3000")
+ end
end
context 'when requesting stopped environments scope' do
diff --git a/spec/controllers/projects/merge_requests_controller_spec.rb b/spec/controllers/projects/merge_requests_controller_spec.rb
index 08024a2148b..a25db7a65fb 100644
--- a/spec/controllers/projects/merge_requests_controller_spec.rb
+++ b/spec/controllers/projects/merge_requests_controller_spec.rb
@@ -126,7 +126,7 @@ describe Projects::MergeRequestsController do
recorded = ActiveRecord::QueryRecorder.new { go(format: :json) }
- expect(recorded.count).to be_within(5).of(50)
+ expect(recorded.count).to be_within(5).of(59)
expect(recorded.cached_count).to eq(0)
end
end
diff --git a/spec/controllers/projects/services_controller_spec.rb b/spec/controllers/projects/services_controller_spec.rb
index 2d892f4a2b7..23b463c0082 100644
--- a/spec/controllers/projects/services_controller_spec.rb
+++ b/spec/controllers/projects/services_controller_spec.rb
@@ -3,7 +3,9 @@ require 'spec_helper'
describe Projects::ServicesController do
let(:project) { create(:project, :repository) }
let(:user) { create(:user) }
- let(:service) { create(:service, project: project) }
+ let(:service) { create(:hipchat_service, project: project) }
+ let(:hipchat_client) { { '#room' => double(send: true) } }
+ let(:service_params) { { token: 'hipchat_token_p', room: '#room' } }
before do
sign_in(user)
@@ -13,97 +15,81 @@ describe Projects::ServicesController do
controller.instance_variable_set(:@service, service)
end
- shared_examples_for 'services controller' do |referrer|
- before do
- request.env["HTTP_REFERER"] = referrer
- end
-
- describe "#test" do
- context 'when can_test? returns false' do
- it 'renders 404' do
- allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
+ describe '#test' do
+ context 'when can_test? returns false' do
+ it 'renders 404' do
+ allow_any_instance_of(Service).to receive(:can_test?).and_return(false)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to have_http_status(404)
- end
+ expect(response).to have_http_status(404)
end
+ end
- context 'success' do
- context 'with empty project' do
- let(:project) { create(:empty_project) }
-
- context 'with chat notification service' do
- let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
-
- it 'redirects and show success message' do
- allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
-
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ context 'success' do
+ context 'with empty project' do
+ let(:project) { create(:empty_project) }
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
- end
- end
+ context 'with chat notification service' do
+ let(:service) { project.create_microsoft_teams_service(webhook: 'http://webhook.com') }
- it 'redirects and show success message' do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ allow_any_instance_of(MicrosoftTeams::Notifier).to receive(:ping).and_return(true)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- it "redirects and show success message" do
- expect(service).to receive(:test).and_return(success: true, result: 'done')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:notice]).to eq('We sent a request to the provided URL')
+ expect(response.status).to eq(200)
end
end
- context 'failure' do
- it "redirects and show failure message" do
- expect(service).to receive(:test).and_return(success: false, result: 'Bad test')
+ it 'returns success' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_return(hipchat_client)
- get :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, format: :html
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
- expect(response).to redirect_to(root_path)
- expect(flash[:alert]).to eq('We tried to send a request to the provided URL but an error occurred: Bad test')
- end
+ expect(response.status).to eq(200)
end
end
- end
- describe 'referrer defined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { "/" }
- end
- end
+ context 'failure' do
+ it 'returns success status code and the error message' do
+ expect(HipChat::Client).to receive(:new).with('hipchat_token_p', anything).and_raise('Bad test')
- describe 'referrer undefined' do
- it_should_behave_like 'services controller' do
- let!(:referrer) { nil }
+ put :test, namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: service_params
+
+ expect(response.status).to eq(200)
+ expect(JSON.parse(response.body)).
+ to eq('error' => true, 'message' => 'Test failed.', 'service_response' => 'Bad test')
+ end
end
end
describe 'PUT #update' do
- context 'on successful update' do
- it 'sets the flash' do
- expect(service).to receive(:to_param).and_return('hipchat')
- expect(service).to receive(:event_names).and_return(HipchatService.event_names)
+ context 'when param `active` is set to true' do
+ it 'activates the service and redirects to integrations paths' do
+ put :update,
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: true }
+
+ expect(response).to redirect_to(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(flash[:notice]).to eq 'HipChat activated.'
+ end
+ end
+ context 'when param `active` is set to false' do
+ it 'does not activate the service but saves the settings' do
put :update,
- namespace_id: project.namespace.id,
- project_id: project.id,
- id: service.id,
- service: { active: false }
+ namespace_id: project.namespace.id, project_id: project.id, id: service.id, service: { active: false }
- expect(flash[:notice]).to eq 'Successfully updated.'
+ expect(flash[:notice]).to eq 'HipChat settings saved, but not activated.'
end
end
end
diff --git a/spec/db/production/settings.rb b/spec/db/production/settings.rb
index 007b35bbb77..3cbb173c4cc 100644
--- a/spec/db/production/settings.rb
+++ b/spec/db/production/settings.rb
@@ -1,5 +1,4 @@
require 'spec_helper'
-require 'rainbow/ext/string'
describe 'seed production settings', lib: true do
include StubENV
diff --git a/spec/factories/ci/variables.rb b/spec/factories/ci/variables.rb
index c5fba597c1c..f83366136fd 100644
--- a/spec/factories/ci/variables.rb
+++ b/spec/factories/ci/variables.rb
@@ -3,6 +3,10 @@ FactoryGirl.define do
sequence(:key) { |n| "VARIABLE_#{n}" }
value 'VARIABLE_VALUE'
+ trait(:protected) do
+ protected true
+ end
+
project factory: :empty_project
end
end
diff --git a/spec/factories/services.rb b/spec/factories/services.rb
index 3fad4d2d658..e7366a7fd1c 100644
--- a/spec/factories/services.rb
+++ b/spec/factories/services.rb
@@ -33,4 +33,10 @@ FactoryGirl.define do
project_key: 'jira-key'
)
end
+
+ factory :hipchat_service do
+ project factory: :empty_project
+ type 'HipchatService'
+ token 'test_token'
+ end
end
diff --git a/spec/features/admin/admin_users_spec.rb b/spec/features/admin/admin_users_spec.rb
index 12cf59f42b0..376e80571d0 100644
--- a/spec/features/admin/admin_users_spec.rb
+++ b/spec/features/admin/admin_users_spec.rb
@@ -21,6 +21,8 @@ describe "Admin::Users", feature: true do
expect(page).to have_content(current_user.name)
expect(page).to have_content(user.email)
expect(page).to have_content(user.name)
+ expect(page).to have_link('Block', href: block_admin_user_path(user))
+ expect(page).to have_link('Delete', href: admin_user_path(user))
end
describe 'Two-factor Authentication filters' do
diff --git a/spec/features/boards/modal_filter_spec.rb b/spec/features/boards/modal_filter_spec.rb
index ce132bfd979..b6de6143354 100644
--- a/spec/features/boards/modal_filter_spec.rb
+++ b/spec/features/boards/modal_filter_spec.rb
@@ -89,7 +89,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
@@ -125,7 +125,7 @@ describe 'Issue Boards add issue modal filtering', :feature, :js do
page.within('.add-issues-modal') do
wait_for_requests
- expect(page).to have_selector('.js-visual-token', text: user2.username)
+ expect(page).to have_selector('.js-visual-token', text: user2.name)
expect(page).to have_selector('.card', count: 1)
end
end
diff --git a/spec/features/commits_spec.rb b/spec/features/commits_spec.rb
index e6c4ab24de5..2772f05982a 100644
--- a/spec/features/commits_spec.rb
+++ b/spec/features/commits_spec.rb
@@ -76,7 +76,7 @@ describe 'Commits' do
end
end
- describe 'Commit builds' do
+ describe 'Commit builds', :feature, :js do
before do
visit ci_status_path(pipeline)
end
@@ -85,7 +85,6 @@ describe 'Commits' do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name
- expect(page).to have_content pipeline.created_at.strftime('%b %d, %Y')
end
end
@@ -102,7 +101,7 @@ describe 'Commits' do
end
describe 'Cancel all builds' do
- it 'cancels commit' do
+ it 'cancels commit', :js do
visit ci_status_path(pipeline)
click_on 'Cancel running'
expect(page).to have_content 'canceled'
@@ -110,9 +109,9 @@ describe 'Commits' do
end
describe 'Cancel build' do
- it 'cancels build' do
+ it 'cancels build', :js do
visit ci_status_path(pipeline)
- find('a.btn[title="Cancel"]').click
+ find('.js-btn-cancel-pipeline').click
expect(page).to have_content 'canceled'
end
end
@@ -152,17 +151,20 @@ describe 'Commits' do
visit ci_status_path(pipeline)
end
- it do
+ it 'Renders header', :feature, :js do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name
- expect(page).to have_link('Download artifacts')
expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry')
end
+
+ it do
+ expect(page).to have_link('Download artifacts')
+ end
end
- context 'when accessing internal project with disallowed access' do
+ context 'when accessing internal project with disallowed access', :feature, :js do
before do
project.update(
visibility_level: Gitlab::VisibilityLevel::INTERNAL,
@@ -175,7 +177,7 @@ describe 'Commits' do
expect(page).to have_content pipeline.sha[0..7]
expect(page).to have_content pipeline.git_commit_message
expect(page).to have_content pipeline.user.name
- expect(page).not_to have_link('Download artifacts')
+
expect(page).not_to have_link('Cancel running')
expect(page).not_to have_link('Retry')
end
diff --git a/spec/features/container_registry_spec.rb b/spec/features/container_registry_spec.rb
index b86609e07c5..fa7adbe71ea 100644
--- a/spec/features/container_registry_spec.rb
+++ b/spec/features/container_registry_spec.rb
@@ -19,7 +19,7 @@ describe "Container Registry" do
scenario 'user visits container registry main page' do
visit_container_registry
- expect(page).to have_content 'No container image repositories'
+ expect(page).to have_content 'No container images'
end
end
diff --git a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
index 4d38df05928..44353d880c2 100644
--- a/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_assignee_spec.rb
@@ -157,6 +157,25 @@ describe 'Dropdown assignee', :feature, :js do
end
end
+ describe 'selecting from dropdown without Ajax call' do
+ before do
+ Gitlab::Testing::RequestBlockerMiddleware.block_requests!
+ filtered_search.set('assignee:')
+ end
+
+ after do
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ it 'selects current user' do
+ find('#js-dropdown-assignee .filter-dropdown-item', text: user.username).click
+
+ expect(page).to have_css(js_dropdown_assignee, visible: false)
+ expect_tokens([{ name: 'assignee', value: user.username }])
+ expect_filtered_search_input_empty
+ end
+ end
+
describe 'input has existing content' do
it 'opens assignee dropdown with existing search term' do
filtered_search.set('searchTerm assignee:')
diff --git a/spec/features/issues/filtered_search/dropdown_author_spec.rb b/spec/features/issues/filtered_search/dropdown_author_spec.rb
index 358b244fb5b..6b707c4be4a 100644
--- a/spec/features/issues/filtered_search/dropdown_author_spec.rb
+++ b/spec/features/issues/filtered_search/dropdown_author_spec.rb
@@ -135,6 +135,25 @@ describe 'Dropdown author', js: true, feature: true do
end
end
+ describe 'selecting from dropdown without Ajax call' do
+ before do
+ Gitlab::Testing::RequestBlockerMiddleware.block_requests!
+ filtered_search.set('author:')
+ end
+
+ after do
+ Gitlab::Testing::RequestBlockerMiddleware.allow_requests!
+ end
+
+ it 'selects current user' do
+ find('#js-dropdown-author .filter-dropdown-item', text: user.username).click
+
+ expect(page).to have_css(js_dropdown_author, visible: false)
+ expect_tokens([{ name: 'author', value: user.username }])
+ expect_filtered_search_input_empty
+ end
+ end
+
describe 'input has existing content' do
it 'opens author dropdown with existing search term' do
filtered_search.set('searchTerm author:')
diff --git a/spec/features/issues/filtered_search/filter_issues_spec.rb b/spec/features/issues/filtered_search/filter_issues_spec.rb
index 7958ad7e24f..e5e4ba06b5a 100644
--- a/spec/features/issues/filtered_search/filter_issues_spec.rb
+++ b/spec/features/issues/filtered_search/filter_issues_spec.rb
@@ -6,7 +6,7 @@ describe 'Filter issues', js: true, feature: true do
let!(:group) { create(:group) }
let!(:project) { create(:project, group: group) }
- let!(:user) { create(:user, username: 'joe') }
+ let!(:user) { create(:user, username: 'joe', name: 'Joe') }
let!(:user2) { create(:user, username: 'jane') }
let!(:label) { create(:label, project: project) }
let!(:wontfix) { create(:label, project: project, title: "Won't fix") }
diff --git a/spec/features/issues/filtered_search/visual_tokens_spec.rb b/spec/features/issues/filtered_search/visual_tokens_spec.rb
index 96e87c82d2c..dbbafc9e004 100644
--- a/spec/features/issues/filtered_search/visual_tokens_spec.rb
+++ b/spec/features/issues/filtered_search/visual_tokens_spec.rb
@@ -2,6 +2,7 @@ require 'rails_helper'
describe 'Visual tokens', js: true, feature: true do
include FilteredSearchHelpers
+ include WaitForRequests
let!(:project) { create(:empty_project) }
let!(:user) { create(:user, name: 'administrator', username: 'root') }
@@ -70,7 +71,8 @@ describe 'Visual tokens', js: true, feature: true do
end
it 'changes value in visual token' do
- expect(first('.tokens-container .filtered-search-token .value').text).to eq("@#{user_rock.username}")
+ wait_for_requests
+ expect(first('.tokens-container .filtered-search-token .value').text).to eq("#{user_rock.name}")
end
it 'moves input to the right' do
diff --git a/spec/features/merge_requests/discussion_spec.rb b/spec/features/merge_requests/discussion_spec.rb
index 1a09cc54c2e..9db235f35ba 100644
--- a/spec/features/merge_requests/discussion_spec.rb
+++ b/spec/features/merge_requests/discussion_spec.rb
@@ -5,7 +5,7 @@ feature 'Merge Request Discussions', feature: true do
login_as :admin
end
- context "Diff discussions" do
+ describe "Diff discussions" do
let(:merge_request) { create(:merge_request, importing: true) }
let(:project) { merge_request.source_project }
let!(:old_merge_request_diff) { merge_request.merge_request_diffs.create(diff_refs: outdated_diff_refs) }
@@ -48,4 +48,43 @@ feature 'Merge Request Discussions', feature: true do
end
end
end
+
+ describe 'Commit comments displayed in MR context', :js do
+ let(:merge_request) { create(:merge_request) }
+ let(:project) { merge_request.project }
+
+ shared_examples 'a functional discussion' do
+ let(:discussion_id) { note.discussion_id(merge_request) }
+
+ it 'is displayed' do
+ expect(page).to have_css(".discussion[data-discussion-id='#{discussion_id}']")
+ end
+
+ it 'can be replied to' do
+ within(".discussion[data-discussion-id='#{discussion_id}']") do
+ click_button 'Reply...'
+ fill_in 'note[note]', with: 'Test!'
+ click_button 'Comment'
+
+ expect(page).to have_css('.note', count: 2)
+ end
+ end
+ end
+
+ before(:each) do
+ visit namespace_project_merge_request_path(project.namespace, project, merge_request)
+ end
+
+ context 'a regular commit comment' do
+ let(:note) { create(:note_on_commit, project: project) }
+
+ it_behaves_like 'a functional discussion'
+ end
+
+ context 'a commit diff comment' do
+ let(:note) { create(:diff_note_on_commit, project: project) }
+
+ it_behaves_like 'a functional discussion'
+ end
+ end
end
diff --git a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
index d94204230f6..53c5a52ce3a 100644
--- a/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
+++ b/spec/features/projects/blobs/blob_line_permalink_updater_spec.rb
@@ -55,7 +55,7 @@ feature 'Blob button line permalinks (BlobLinePermalinkUpdater)', feature: true,
end
end
- describe 'Click "Blame" button' do
+ describe 'Click "Annotate" button' do
it 'works with no initial line number fragment hash' do
visit_blob
diff --git a/spec/features/projects/files/browse_files_spec.rb b/spec/features/projects/files/browse_files_spec.rb
index c0a9327249c..30a1eedbb48 100644
--- a/spec/features/projects/files/browse_files_spec.rb
+++ b/spec/features/projects/files/browse_files_spec.rb
@@ -12,7 +12,7 @@ feature 'user browses project', feature: true, js: true do
scenario "can see blame of '.gitignore'" do
click_link ".gitignore"
- click_link 'Blame'
+ click_link 'Annotate'
expect(page).to have_content "*.rb"
expect(page).to have_content "Dmitriy Zaporozhets"
diff --git a/spec/features/projects/pipelines/pipeline_spec.rb b/spec/features/projects/pipelines/pipeline_spec.rb
index cfac54ef259..36a3ddca6ef 100644
--- a/spec/features/projects/pipelines/pipeline_spec.rb
+++ b/spec/features/projects/pipelines/pipeline_spec.rb
@@ -229,7 +229,6 @@ describe 'Pipeline', :feature, :js do
before { find('.js-retry-button').trigger('click') }
it { expect(page).not_to have_content('Retry') }
- it { expect(page).to have_selector('.retried') }
end
end
@@ -240,7 +239,6 @@ describe 'Pipeline', :feature, :js do
before { click_on 'Cancel running' }
it { expect(page).not_to have_content('Cancel running') }
- it { expect(page).to have_selector('.ci-canceled') }
end
end
diff --git a/spec/features/projects/services/jira_service_spec.rb b/spec/features/projects/services/jira_service_spec.rb
new file mode 100644
index 00000000000..c96d87e5708
--- /dev/null
+++ b/spec/features/projects/services/jira_service_spec.rb
@@ -0,0 +1,92 @@
+require 'spec_helper'
+
+feature 'Setup Jira service', :feature, :js do
+ let(:user) { create(:user) }
+ let(:project) { create(:empty_project) }
+ let(:service) { project.create_jira_service }
+
+ let(:url) { 'http://jira.example.com' }
+ let(:project_url) { 'http://username:password@jira.example.com/rest/api/2/project/GitLabProject' }
+
+ def fill_form(active = true)
+ check 'Active' if active
+
+ fill_in 'service_url', with: url
+ fill_in 'service_project_key', with: 'GitLabProject'
+ fill_in 'service_username', with: 'username'
+ fill_in 'service_password', with: 'password'
+ fill_in 'service_jira_issue_transition_id', with: '25'
+ end
+
+ before do
+ project.team << [user, :master]
+ login_as(user)
+
+ visit namespace_project_settings_integrations_path(project.namespace, project)
+ end
+
+ describe 'user sets and activates Jira Service' do
+ context 'when Jira connection test succeeds' do
+ before do
+ WebMock.stub_request(:get, project_url)
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+
+ context 'when Jira connection test fails' do
+ before do
+ WebMock.stub_request(:get, project_url).to_return(status: 401)
+ end
+
+ it 'shows errors when some required fields are not filled in' do
+ click_link('JIRA')
+
+ check 'Active'
+ fill_in 'service_password', with: 'password'
+ click_button('Test settings and save changes')
+
+ page.within('.service-settings') do
+ expect(page).to have_content('This field is required.')
+ end
+ end
+
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form
+ click_button('Test settings and save changes')
+ wait_for_requests
+
+ expect(find('.flash-container-page')).to have_content 'Test failed.'
+ expect(find('.flash-container-page')).to have_content 'Save anyway'
+
+ find('.flash-alert .flash-action').trigger('click')
+ wait_for_requests
+
+ expect(page).to have_content('JIRA activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+
+ describe 'user sets Jira Service but keeps it disabled' do
+ context 'when Jira connection test succeeds' do
+ it 'activates the JIRA service' do
+ click_link('JIRA')
+ fill_form(false)
+ click_button('Save changes')
+
+ expect(page).to have_content('JIRA settings saved, but not activated.')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ end
+ end
+ end
+end
diff --git a/spec/features/projects/services/mattermost_slash_command_spec.rb b/spec/features/projects/services/mattermost_slash_command_spec.rb
index dc3854262e7..1fe82222e59 100644
--- a/spec/features/projects/services/mattermost_slash_command_spec.rb
+++ b/spec/features/projects/services/mattermost_slash_command_spec.rb
@@ -24,15 +24,25 @@ feature 'Setup Mattermost slash commands', :feature, :js do
expect(token_placeholder).to eq('XXxxXXxxXXxxXXxxXXxxXXxx')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
token = ('a'..'z').to_a.join
fill_in 'service_token', with: token
- click_on 'Save'
+ click_on 'Save changes'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ token = ('a'..'z').to_a.join
+
+ fill_in 'service_token', with: token
+ check 'service_active'
+ click_on 'Save changes'
- expect(value).to eq(token)
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Mattermost slash commands activated.')
end
it 'shows the add to mattermost button' do
diff --git a/spec/features/projects/services/slack_slash_command_spec.rb b/spec/features/projects/services/slack_slash_command_spec.rb
index db903a0c8f0..f53b820c460 100644
--- a/spec/features/projects/services/slack_slash_command_spec.rb
+++ b/spec/features/projects/services/slack_slash_command_spec.rb
@@ -21,13 +21,21 @@ feature 'Slack slash commands', feature: true do
expect(page).to have_content('This service allows users to perform common')
end
- it 'shows the token after saving' do
+ it 'redirects to the integrations page after saving but not activating' do
fill_in 'service_token', with: 'token'
click_on 'Save'
- value = find_field('service_token').value
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands settings saved, but not activated.')
+ end
+
+ it 'redirects to the integrations page after activating' do
+ fill_in 'service_token', with: 'token'
+ check 'service_active'
+ click_on 'Save'
- expect(value).to eq('token')
+ expect(current_path).to eq(namespace_project_settings_integrations_path(project.namespace, project))
+ expect(page).to have_content('Slack slash commands activated.')
end
it 'shows the correct trigger url' do
diff --git a/spec/features/variables_spec.rb b/spec/features/variables_spec.rb
index b83a230c1f8..d0c982919db 100644
--- a/spec/features/variables_spec.rb
+++ b/spec/features/variables_spec.rb
@@ -19,7 +19,7 @@ describe 'Project variables', js: true do
end
end
- it 'adds new variable' do
+ it 'adds new secret variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
click_button('Add new variable')
@@ -27,6 +27,7 @@ describe 'Project variables', js: true do
expect(page).to have_content('Variables were successfully updated.')
page.within('.variables-table') do
expect(page).to have_content('key')
+ expect(page).to have_content('No')
end
end
@@ -41,6 +42,19 @@ describe 'Project variables', js: true do
end
end
+ it 'adds new protected variable' do
+ fill_in('variable_key', with: 'key')
+ fill_in('variable_value', with: 'value')
+ check('Protected')
+ click_button('Add new variable')
+
+ expect(page).to have_content('Variables were successfully updated.')
+ page.within('.variables-table') do
+ expect(page).to have_content('key')
+ expect(page).to have_content('Yes')
+ end
+ end
+
it 'reveals and hides new variable' do
fill_in('variable_key', with: 'key')
fill_in('variable_value', with: 'key value')
@@ -85,7 +99,7 @@ describe 'Project variables', js: true do
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
- expect(project.variables.first.value).to eq('key value')
+ expect(project.variables(true).first.value).to eq('key value')
end
it 'edits variable with empty value' do
@@ -98,6 +112,34 @@ describe 'Project variables', js: true do
click_button('Save variable')
expect(page).to have_content('Variable was successfully updated.')
- expect(project.variables.first.value).to eq('')
+ expect(project.variables(true).first.value).to eq('')
+ end
+
+ it 'edits variable to be protected' do
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
+ end
+
+ expect(page).to have_content('Update variable')
+ check('Protected')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables(true).first).to be_protected
+ end
+
+ it 'edits variable to be unprotected' do
+ project.variables.first.update(protected: true)
+
+ page.within('.variables-table') do
+ find('.btn-variable-edit').click
+ end
+
+ expect(page).to have_content('Update variable')
+ uncheck('Protected')
+ click_button('Save variable')
+
+ expect(page).to have_content('Variable was successfully updated.')
+ expect(project.variables(true).first).not_to be_protected
end
end
diff --git a/spec/helpers/avatars_helper_spec.rb b/spec/helpers/avatars_helper_spec.rb
index 6157abfe339..049475a5408 100644
--- a/spec/helpers/avatars_helper_spec.rb
+++ b/spec/helpers/avatars_helper_spec.rb
@@ -1,6 +1,8 @@
require 'rails_helper'
describe AvatarsHelper do
+ include ApplicationHelper
+
let(:user) { create(:user) }
describe '#user_avatar' do
@@ -18,4 +20,103 @@ describe AvatarsHelper do
is_expected.to include(CGI.escapeHTML(user.avatar_url(size: 16)))
end
end
+
+ describe '#user_avatar_without_link' do
+ let(:options) { { user: user } }
+ subject { helper.user_avatar_without_link(options) }
+
+ it 'displays user avatar' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+
+ context 'with css_class parameter' do
+ let(:options) { { user: user, css_class: '.cat-pics' } }
+
+ it 'uses provided css_class' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: "avatar has-tooltip s16 #{options[:css_class]}",
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with lazy parameter' do
+ let(:options) { { user: user, lazy: true } }
+
+ it 'uses data-src instead of src' do
+ is_expected.to eq image_tag(
+ '',
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body', src: avatar_icon(user, 16) }
+ )
+ end
+ end
+
+ context 'with size parameter' do
+ let(:options) { { user: user, size: 99 } }
+
+ it 'uses provided size' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, options[:size]),
+ class: "avatar has-tooltip s#{options[:size]} ",
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with url parameter' do
+ let(:options) { { user: user, url: '/over/the/rainbow.png' } }
+
+ it 'uses provided url' do
+ is_expected.to eq image_tag(
+ options[:url],
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ context 'with user_name parameter' do
+ let(:options) { { user_name: 'Tinky Winky', user_email: 'no@f.un' } }
+
+ context 'with user parameter' do
+ let(:options) { { user: user, user_name: 'Tinky Winky' } }
+
+ it 'prefers user parameter' do
+ is_expected.to eq image_tag(
+ avatar_icon(user, 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{user.name}'s avatar",
+ title: user.name,
+ data: { container: 'body' }
+ )
+ end
+ end
+
+ it 'uses user_name and user_email parameter if user is not present' do
+ is_expected.to eq image_tag(
+ avatar_icon(options[:user_email], 16),
+ class: 'avatar has-tooltip s16 ',
+ alt: "#{options[:user_name]}'s avatar",
+ title: options[:user_name],
+ data: { container: 'body' }
+ )
+ end
+ end
+ end
end
diff --git a/spec/helpers/blob_helper_spec.rb b/spec/helpers/blob_helper_spec.rb
index 41b5df12522..bd3a3d24b84 100644
--- a/spec/helpers/blob_helper_spec.rb
+++ b/spec/helpers/blob_helper_spec.rb
@@ -118,8 +118,8 @@ describe BlobHelper do
Class.new(BlobViewer::Base) do
include BlobViewer::ServerSide
- self.overridable_max_size = 1.megabyte
- self.max_size = 5.megabytes
+ self.collapse_limit = 1.megabyte
+ self.size_limit = 5.megabytes
self.type = :rich
end
end
@@ -129,7 +129,7 @@ describe BlobHelper do
describe '#blob_render_error_reason' do
context 'for error :too_large' do
- context 'when the blob size is larger than the absolute max size' do
+ context 'when the blob size is larger than the absolute size limit' do
let(:blob) { fake_blob(size: 10.megabytes) }
it 'returns an error message' do
@@ -137,7 +137,7 @@ describe BlobHelper do
end
end
- context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the size limit' do
let(:blob) { fake_blob(size: 2.megabytes) }
it 'returns an error message' do
@@ -168,21 +168,19 @@ describe BlobHelper do
controller.params[:id] = File.join('master', blob.path)
end
- context 'for error :too_large' do
- context 'when the max size can be overridden' do
- let(:blob) { fake_blob(size: 2.megabytes) }
+ context 'for error :collapsed' do
+ let(:blob) { fake_blob(size: 2.megabytes) }
- it 'includes a "load it anyway" link' do
- expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
- end
+ it 'includes a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).to include(/load it anyway/)
end
+ end
- context 'when the max size cannot be overridden' do
- let(:blob) { fake_blob(size: 10.megabytes) }
+ context 'for error :too_large' do
+ let(:blob) { fake_blob(size: 10.megabytes) }
- it 'does not include a "load it anyway" link' do
- expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
- end
+ it 'does not include a "load it anyway" link' do
+ expect(helper.blob_render_error_options(viewer)).not_to include(/load it anyway/)
end
context 'when the viewer is rich' do
diff --git a/spec/helpers/diff_helper_spec.rb b/spec/helpers/diff_helper_spec.rb
index dd6566d25bb..a74615e07f9 100644
--- a/spec/helpers/diff_helper_spec.rb
+++ b/spec/helpers/diff_helper_spec.rb
@@ -33,17 +33,17 @@ describe DiffHelper do
describe 'diff_options' do
it 'returns no collapse false' do
- expect(diff_options).to include(no_collapse: false)
+ expect(diff_options).to include(expanded: false)
end
- it 'returns no collapse true if expand_all_diffs' do
- allow(controller).to receive(:params) { { expand_all_diffs: true } }
- expect(diff_options).to include(no_collapse: true)
+ it 'returns no collapse true if expanded' do
+ allow(controller).to receive(:params) { { expanded: true } }
+ expect(diff_options).to include(expanded: true)
end
it 'returns no collapse true if action name diff_for_path' do
allow(controller).to receive(:action_name) { 'diff_for_path' }
- expect(diff_options).to include(no_collapse: true)
+ expect(diff_options).to include(expanded: true)
end
it 'returns paths if action name diff_for_path and param old path' do
@@ -129,6 +129,33 @@ describe DiffHelper do
end
end
+ describe '#parallel_diff_discussions' do
+ let(:discussion) { { 'abc_3_3' => 'comment' } }
+ let(:diff_file) { double(line_code: 'abc_3_3') }
+
+ before do
+ helper.instance_variable_set(:@grouped_diff_discussions, discussion)
+ end
+
+ it 'does not put comments on nonewline lines' do
+ left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
+ right = Gitlab::Diff::Line.new('\\nonewline', 'new-nonewline', 3, 3, 3)
+
+ result = helper.parallel_diff_discussions(left, right, diff_file)
+
+ expect(result).to eq([nil, nil])
+ end
+
+ it 'puts comments on added lines' do
+ left = Gitlab::Diff::Line.new('\\nonewline', 'old-nonewline', 3, 3, 3)
+ right = Gitlab::Diff::Line.new('new line', 'add', 3, 3, 3)
+
+ result = helper.parallel_diff_discussions(left, right, diff_file)
+
+ expect(result).to eq([nil, 'comment'])
+ end
+ end
+
describe "#diff_match_line" do
let(:old_pos) { 40 }
let(:new_pos) { 50 }
diff --git a/spec/javascripts/droplab/plugins/ajax_filter_spec.js b/spec/javascripts/droplab/plugins/ajax_filter_spec.js
new file mode 100644
index 00000000000..8155d98b543
--- /dev/null
+++ b/spec/javascripts/droplab/plugins/ajax_filter_spec.js
@@ -0,0 +1,72 @@
+import AjaxCache from '~/lib/utils/ajax_cache';
+import AjaxFilter from '~/droplab/plugins/ajax_filter';
+
+describe('AjaxFilter', () => {
+ let dummyConfig;
+ const dummyData = 'dummy data';
+ let dummyList;
+
+ beforeEach(() => {
+ dummyConfig = {
+ endpoint: 'dummy endpoint',
+ searchKey: 'dummy search key',
+ };
+ dummyList = {
+ data: [],
+ list: document.createElement('div'),
+ };
+
+ AjaxFilter.hook = {
+ config: {
+ AjaxFilter: dummyConfig,
+ },
+ list: dummyList,
+ };
+ });
+
+ describe('trigger', () => {
+ let ajaxSpy;
+
+ beforeEach(() => {
+ spyOn(AjaxCache, 'retrieve').and.callFake(url => ajaxSpy(url));
+ spyOn(AjaxFilter, '_loadData');
+
+ dummyConfig.onLoadingFinished = jasmine.createSpy('spy');
+
+ const dynamicList = document.createElement('div');
+ dynamicList.dataset.dynamic = true;
+ dummyList.list.appendChild(dynamicList);
+ });
+
+ it('calls onLoadingFinished after loading data', (done) => {
+ ajaxSpy = (url) => {
+ expect(url).toBe('dummy endpoint?dummy search key=');
+ return Promise.resolve(dummyData);
+ };
+
+ AjaxFilter.trigger()
+ .then(() => {
+ expect(dummyConfig.onLoadingFinished.calls.count()).toBe(1);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not call onLoadingFinished if Ajax call fails', (done) => {
+ const dummyError = new Error('My dummy is sick! :-(');
+ ajaxSpy = (url) => {
+ expect(url).toBe('dummy endpoint?dummy search key=');
+ return Promise.reject(dummyError);
+ };
+
+ AjaxFilter.trigger()
+ .then(done.fail)
+ .catch((error) => {
+ expect(error).toBe(dummyError);
+ expect(dummyConfig.onLoadingFinished.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+ });
+});
diff --git a/spec/javascripts/environments/environments_store_spec.js b/spec/javascripts/environments/environments_store_spec.js
index f617c4bdffe..6e855530b21 100644
--- a/spec/javascripts/environments/environments_store_spec.js
+++ b/spec/javascripts/environments/environments_store_spec.js
@@ -123,4 +123,13 @@ describe('Store', () => {
expect(store.state.paginationInformation).toEqual(expectedResult);
});
});
+
+ describe('getOpenFolders', () => {
+ it('should return open folder', () => {
+ store.storeEnvironments(serverData);
+
+ store.toggleFolder(store.state.environments[1]);
+ expect(store.getOpenFolders()[0]).toEqual(store.state.environments[1]);
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/dropdown_utils_spec.js b/spec/javascripts/filtered_search/dropdown_utils_spec.js
index bb02abdeea2..f55726379f3 100644
--- a/spec/javascripts/filtered_search/dropdown_utils_spec.js
+++ b/spec/javascripts/filtered_search/dropdown_utils_spec.js
@@ -2,8 +2,12 @@ import '~/extensions/array';
import '~/filtered_search/dropdown_utils';
import '~/filtered_search/filtered_search_tokenizer';
import '~/filtered_search/filtered_search_dropdown_manager';
+import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Dropdown Utils', () => {
+ const issueListFixture = 'issues/issue_list.html.raw';
+ preloadFixtures(issueListFixture);
+
describe('getEscapedText', () => {
it('should return same word when it has no space', () => {
const escaped = gl.DropdownUtils.getEscapedText('textWithoutSpace');
@@ -314,4 +318,29 @@ describe('Dropdown Utils', () => {
});
});
});
+
+ describe('getSearchQuery', () => {
+ let authorToken;
+
+ beforeEach(() => {
+ loadFixtures(issueListFixture);
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ const searchTermToken = FilteredSearchSpecHelper.createSearchVisualToken('search term');
+
+ const tokensContainer = document.querySelector('.tokens-container');
+ tokensContainer.appendChild(searchTermToken);
+ tokensContainer.appendChild(authorToken);
+ });
+
+ it('uses original value if present', () => {
+ const originalValue = 'original dance';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ const searchQuery = gl.DropdownUtils.getSearchQuery();
+
+ expect(searchQuery).toBe(' search term author:original dance');
+ });
+ });
});
diff --git a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
index c5fa2b17106..fa4343ffbc8 100644
--- a/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
+++ b/spec/javascripts/filtered_search/filtered_search_visual_tokens_spec.js
@@ -1,10 +1,22 @@
import AjaxCache from '~/lib/utils/ajax_cache';
+import UsersCache from '~/lib/utils/users_cache';
import '~/filtered_search/filtered_search_visual_tokens';
import FilteredSearchSpecHelper from '../helpers/filtered_search_spec_helper';
describe('Filtered Search Visual Tokens', () => {
+ const subject = gl.FilteredSearchVisualTokens;
+
+ const findElements = (tokenElement) => {
+ const tokenNameElement = tokenElement.querySelector('.name');
+ const tokenValueContainer = tokenElement.querySelector('.value-container');
+ const tokenValueElement = tokenValueContainer.querySelector('.value');
+ return { tokenNameElement, tokenValueContainer, tokenValueElement };
+ };
+
let tokensContainer;
+ let authorToken;
+ let bugLabelToken;
beforeEach(() => {
setFixtures(`
@@ -13,12 +25,15 @@ describe('Filtered Search Visual Tokens', () => {
</ul>
`);
tokensContainer = document.querySelector('.tokens-container');
+
+ authorToken = FilteredSearchSpecHelper.createFilterVisualToken('author', '@user');
+ bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
});
describe('getLastVisualTokenBeforeInput', () => {
it('returns when there are no visual tokens', () => {
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(null);
expect(isLastVisualTokenValid).toEqual(true);
@@ -27,11 +42,11 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is the last item in tokensContainer', () => {
it('returns when there is one visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug'),
+ bugLabelToken.outerHTML,
);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
@@ -43,7 +58,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
@@ -51,13 +66,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
@@ -66,13 +81,13 @@ describe('Filtered Search Visual Tokens', () => {
it('returns when there are multiple visual tokens and an incomplete visual token', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('assignee')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
const items = document.querySelectorAll('.tokens-container .js-visual-token');
expect(lastVisualToken.isEqualNode(items[items.length - 1])).toEqual(true);
@@ -83,13 +98,13 @@ describe('Filtered Search Visual Tokens', () => {
describe('input is a middle item in tokensContainer', () => {
it('returns last token before input', () => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createInputHTML()}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('author', '@root')}
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(true);
@@ -103,7 +118,7 @@ describe('Filtered Search Visual Tokens', () => {
`);
const { lastVisualToken, isLastVisualTokenValid }
- = gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput();
+ = subject.getLastVisualTokenBeforeInput();
expect(lastVisualToken).toEqual(document.querySelector('.filtered-search-token'));
expect(isLastVisualTokenValid).toEqual(false);
@@ -114,7 +129,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('unselectTokens', () => {
it('does nothing when there are no tokens', () => {
const beforeHTML = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.unselectTokens();
+ subject.unselectTokens();
expect(tokensContainer.innerHTML).toEqual(beforeHTML);
});
@@ -128,7 +143,7 @@ describe('Filtered Search Visual Tokens', () => {
const selected = tokensContainer.querySelector('.js-visual-token .selected');
expect(selected.classList.contains('selected')).toEqual(true);
- gl.FilteredSearchVisualTokens.unselectTokens();
+ subject.unselectTokens();
expect(selected.classList.contains('selected')).toEqual(false);
});
@@ -137,7 +152,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('selectToken', () => {
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~awesome')}
`);
@@ -147,7 +162,7 @@ describe('Filtered Search Visual Tokens', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
firstTokenButton.classList.add('selected');
- gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+ subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(false);
});
@@ -156,7 +171,7 @@ describe('Filtered Search Visual Tokens', () => {
it('adds selected class', () => {
const firstTokenButton = tokensContainer.querySelector('.js-visual-token .selectable');
- gl.FilteredSearchVisualTokens.selectToken(firstTokenButton);
+ subject.selectToken(firstTokenButton);
expect(firstTokenButton.classList.contains('selected')).toEqual(true);
});
@@ -165,7 +180,7 @@ describe('Filtered Search Visual Tokens', () => {
const tokenButtons = tokensContainer.querySelectorAll('.js-visual-token .selectable');
tokenButtons[1].classList.add('selected');
- gl.FilteredSearchVisualTokens.selectToken(tokenButtons[0]);
+ subject.selectToken(tokenButtons[0]);
expect(tokenButtons[0].classList.contains('selected')).toEqual(true);
expect(tokenButtons[1].classList.contains('selected')).toEqual(false);
@@ -181,7 +196,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeSelectedToken();
+ subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
});
@@ -193,7 +208,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .selectable')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeSelectedToken();
+ subject.removeSelectedToken();
expect(tokensContainer.querySelector('.js-visual-token .selectable')).toEqual(null);
});
@@ -205,7 +220,7 @@ describe('Filtered Search Visual Tokens', () => {
beforeEach(() => {
setFixtures(`
<div class="test-area">
- ${gl.FilteredSearchVisualTokens.createVisualTokenElementHTML()}
+ ${subject.createVisualTokenElementHTML()}
</div>
`);
@@ -245,7 +260,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addVisualTokenElement', () => {
it('renders search visual tokens', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('search term', null, true);
+ subject.addVisualTokenElement('search term', null, true);
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
@@ -254,7 +269,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('renders filter visual token name', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('milestone');
+ subject.addVisualTokenElement('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -263,7 +278,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('renders filter visual token name and value', () => {
- gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ subject.addVisualTokenElement('label', 'Frontend');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -274,7 +289,7 @@ describe('Filtered Search Visual Tokens', () => {
it('inserts visual token before input', () => {
tokensContainer.appendChild(FilteredSearchSpecHelper.createFilterVisualToken('assignee', '@root'));
- gl.FilteredSearchVisualTokens.addVisualTokenElement('label', 'Frontend');
+ subject.addVisualTokenElement('label', 'Frontend');
const tokens = tokensContainer.querySelectorAll('.js-visual-token');
const labelToken = tokens[0];
const assigneeToken = tokens[1];
@@ -296,7 +311,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
@@ -308,7 +323,7 @@ describe('Filtered Search Visual Tokens', () => {
`);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
expect(original).toEqual(tokensContainer.innerHTML);
});
@@ -319,7 +334,7 @@ describe('Filtered Search Visual Tokens', () => {
);
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.addValueToPreviousVisualTokenElement('value');
+ subject.addValueToPreviousVisualTokenElement('value');
const updatedToken = tokensContainer.querySelector('.js-visual-token');
expect(updatedToken.querySelector('.name').innerText).toEqual('label');
@@ -330,7 +345,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addFilterVisualToken', () => {
it('creates visual token with just tokenName', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
+ subject.addFilterVisualToken('milestone');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -339,8 +354,8 @@ describe('Filtered Search Visual Tokens', () => {
});
it('creates visual token with just tokenValue', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('milestone');
- gl.FilteredSearchVisualTokens.addFilterVisualToken('%8.17');
+ subject.addFilterVisualToken('milestone');
+ subject.addFilterVisualToken('%8.17');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -349,7 +364,7 @@ describe('Filtered Search Visual Tokens', () => {
});
it('creates full visual token', () => {
- gl.FilteredSearchVisualTokens.addFilterVisualToken('assignee', '@john');
+ subject.addFilterVisualToken('assignee', '@john');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-token')).toEqual(true);
@@ -360,7 +375,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('addSearchVisualToken', () => {
it('creates search visual token', () => {
- gl.FilteredSearchVisualTokens.addSearchVisualToken('search term');
+ subject.addSearchVisualToken('search term');
const token = tokensContainer.querySelector('.js-visual-token');
expect(token.classList.contains('filtered-search-term')).toEqual(true);
@@ -374,7 +389,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search term')}
`);
- gl.FilteredSearchVisualTokens.addSearchVisualToken('append this');
+ subject.addSearchVisualToken('append this');
const token = tokensContainer.querySelector('.filtered-search-term');
expect(token.querySelector('.name').innerText).toEqual('search term append this');
@@ -386,10 +401,26 @@ describe('Filtered Search Visual Tokens', () => {
it('should get last token value', () => {
const value = '~bug';
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
- FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', value),
+ bugLabelToken.outerHTML,
+ );
+
+ expect(subject.getLastTokenPartial()).toEqual(value);
+ });
+
+ it('should get last token original value if available', () => {
+ const originalValue = '@user';
+ const valueContainer = authorToken.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+ const avatar = document.createElement('img');
+ const valueElement = valueContainer.querySelector('.value');
+ valueElement.insertAdjacentElement('afterbegin', avatar);
+ tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(
+ authorToken.outerHTML,
);
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(value);
+ const lastTokenValue = subject.getLastTokenPartial();
+
+ expect(lastTokenValue).toEqual(originalValue);
});
it('should get last token name if there is no value', () => {
@@ -398,11 +429,11 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createNameFilterVisualTokenHTML(name),
);
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual(name);
+ expect(subject.getLastTokenPartial()).toEqual(name);
});
it('should return empty when there are no tokens', () => {
- expect(gl.FilteredSearchVisualTokens.getLastTokenPartial()).toEqual('');
+ expect(subject.getLastTokenPartial()).toEqual('');
});
});
@@ -414,7 +445,7 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .value')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .value')).toEqual(null);
});
@@ -426,14 +457,14 @@ describe('Filtered Search Visual Tokens', () => {
expect(tokensContainer.querySelector('.js-visual-token .name')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.querySelector('.js-visual-token .name')).toEqual(null);
});
it('should not remove anything when there are no tokens', () => {
const html = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.removeLastTokenPartial();
+ subject.removeLastTokenPartial();
expect(tokensContainer.innerHTML).toEqual(html);
});
@@ -442,7 +473,7 @@ describe('Filtered Search Visual Tokens', () => {
describe('tokenizeInput', () => {
it('does not do anything if there is no input', () => {
const original = tokensContainer.innerHTML;
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
expect(tokensContainer.innerHTML).toEqual(original);
});
@@ -454,7 +485,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search');
input.value = 'some value';
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
const newToken = tokensContainer.querySelector('.filtered-search-term');
@@ -470,7 +501,7 @@ describe('Filtered Search Visual Tokens', () => {
const input = document.querySelector('.filtered-search');
input.value = '@john';
- gl.FilteredSearchVisualTokens.tokenizeInput();
+ subject.tokenizeInput();
const updatedToken = tokensContainer.querySelector('.filtered-search-token');
@@ -497,29 +528,39 @@ describe('Filtered Search Visual Tokens', () => {
it('tokenize\'s existing input', () => {
input.value = 'some text';
- spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callThrough();
+ spyOn(subject, 'tokenizeInput').and.callThrough();
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
- expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
+ expect(subject.tokenizeInput).toHaveBeenCalled();
expect(input.value).not.toEqual('some text');
});
it('moves input to the token position', () => {
expect(tokensContainer.children[3].querySelector('.filtered-search')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(tokensContainer.children[1].querySelector('.filtered-search')).not.toEqual(null);
expect(tokensContainer.children[3].querySelector('.filtered-search')).toEqual(null);
});
it('input contains the visual token value', () => {
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(input.value).toEqual('none');
});
+ it('input contains the original value if present', () => {
+ const originalValue = '@user';
+ const valueContainer = token.querySelector('.value-container');
+ valueContainer.dataset.originalValue = originalValue;
+
+ subject.editToken(token);
+
+ expect(input.value).toEqual(originalValue);
+ });
+
describe('selected token is a search term token', () => {
beforeEach(() => {
token = document.querySelector('.filtered-search-term');
@@ -528,7 +569,7 @@ describe('Filtered Search Visual Tokens', () => {
it('token is removed', () => {
expect(tokensContainer.querySelector('.filtered-search-term')).not.toEqual(null);
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(tokensContainer.querySelector('.filtered-search-term')).toEqual(null);
});
@@ -536,7 +577,7 @@ describe('Filtered Search Visual Tokens', () => {
it('input has the same value as removed token', () => {
expect(input.value).toEqual('');
- gl.FilteredSearchVisualTokens.editToken(token);
+ subject.editToken(token);
expect(input.value).toEqual('search');
});
@@ -549,25 +590,25 @@ describe('Filtered Search Visual Tokens', () => {
FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none'),
);
- spyOn(gl.FilteredSearchVisualTokens, 'tokenizeInput').and.callFake(() => {});
- spyOn(gl.FilteredSearchVisualTokens, 'getLastVisualTokenBeforeInput').and.callThrough();
+ spyOn(subject, 'tokenizeInput').and.callFake(() => {});
+ spyOn(subject, 'getLastVisualTokenBeforeInput').and.callThrough();
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
- expect(gl.FilteredSearchVisualTokens.tokenizeInput).toHaveBeenCalled();
- expect(gl.FilteredSearchVisualTokens.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
+ expect(subject.tokenizeInput).toHaveBeenCalled();
+ expect(subject.getLastVisualTokenBeforeInput).not.toHaveBeenCalled();
});
it('tokenize\'s input', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createNameFilterVisualTokenHTML('label')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
document.querySelector('.filtered-search').value = 'none';
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const value = tokensContainer.querySelector('.js-visual-token .value');
expect(value.innerText).toEqual('none');
@@ -577,12 +618,12 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
document.querySelector('.filtered-search').value = 'test';
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const searchValue = tokensContainer.querySelector('.filtered-search-term .name');
expect(searchValue.innerText).toEqual('test');
@@ -592,10 +633,10 @@ describe('Filtered Search Visual Tokens', () => {
tokensContainer.innerHTML = `
${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
${FilteredSearchSpecHelper.createInputHTML()}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', '~bug')}
+ ${bugLabelToken.outerHTML}
`;
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
expect(tokensContainer.children[2].querySelector('.filtered-search')).not.toEqual(null);
});
@@ -607,7 +648,7 @@ describe('Filtered Search Visual Tokens', () => {
${FilteredSearchSpecHelper.createInputHTML('', '~bug')}
`;
- gl.FilteredSearchVisualTokens.moveInputToTheRight();
+ subject.moveInputToTheRight();
const token = tokensContainer.children[1];
expect(token.querySelector('.value').innerText).toEqual('~bug');
@@ -615,42 +656,144 @@ describe('Filtered Search Visual Tokens', () => {
});
describe('renderVisualTokenValue', () => {
- let searchTokens;
+ const keywordToken = FilteredSearchSpecHelper.createFilterVisualToken('search');
+ const milestoneToken = FilteredSearchSpecHelper.createFilterVisualToken('milestone', 'upcoming');
+
+ let updateLabelTokenColorSpy;
+ let updateUserTokenAppearanceSpy;
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('label', 'none')}
- ${FilteredSearchSpecHelper.createSearchVisualTokenHTML('search')}
- ${FilteredSearchSpecHelper.createFilterVisualTokenHTML('milestone', 'upcoming')}
+ ${authorToken.outerHTML}
+ ${bugLabelToken.outerHTML}
+ ${keywordToken.outerHTML}
+ ${milestoneToken.outerHTML}
`);
- searchTokens = document.querySelectorAll('.filtered-search-token');
+ spyOn(subject, 'updateLabelTokenColor');
+ updateLabelTokenColorSpy = subject.updateLabelTokenColor;
+
+ spyOn(subject, 'updateUserTokenAppearance');
+ updateUserTokenAppearanceSpy = subject.updateUserTokenAppearance;
});
- it('renders a token value element', () => {
- spyOn(gl.FilteredSearchVisualTokens, 'updateLabelTokenColor');
- const updateLabelTokenColorSpy = gl.FilteredSearchVisualTokens.updateLabelTokenColor;
+ it('renders a author token value element', () => {
+ const { tokenNameElement, tokenValueContainer, tokenValueElement } =
+ findElements(authorToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
- expect(searchTokens.length).toBe(2);
- Array.prototype.forEach.call(searchTokens, (token) => {
- updateLabelTokenColorSpy.calls.reset();
+ subject.renderVisualTokenValue(authorToken, tokenName, tokenValue);
- const tokenName = token.querySelector('.name').innerText;
- const tokenValue = 'new value';
- gl.FilteredSearchVisualTokens.renderVisualTokenValue(token, tokenName, tokenValue);
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValueElement, tokenValue];
+ expect(updateUserTokenAppearanceSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ });
- const tokenValueElement = token.querySelector('.value');
- expect(tokenValueElement.innerText).toBe(tokenValue);
+ it('renders a label token value element', () => {
+ const { tokenNameElement, tokenValueContainer, tokenValueElement } =
+ findElements(bugLabelToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
- if (tokenName.toLowerCase() === 'label') {
- const tokenValueContainer = token.querySelector('.value-container');
- expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
- const expectedArgs = [tokenValueContainer, tokenValue];
- expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
- } else {
- expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
- }
- });
+ subject.renderVisualTokenValue(bugLabelToken, tokenName, tokenValue);
+
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(1);
+ const expectedArgs = [tokenValueContainer, tokenValue];
+ expect(updateLabelTokenColorSpy.calls.argsFor(0)).toEqual(expectedArgs);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+
+ it('renders a milestone token value element', () => {
+ const { tokenNameElement, tokenValueElement } = findElements(milestoneToken);
+ const tokenName = tokenNameElement.innerText;
+ const tokenValue = 'new value';
+
+ subject.renderVisualTokenValue(milestoneToken, tokenName, tokenValue);
+
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ expect(updateLabelTokenColorSpy.calls.count()).toBe(0);
+ expect(updateUserTokenAppearanceSpy.calls.count()).toBe(0);
+ });
+ });
+
+ describe('updateUserTokenAppearance', () => {
+ let usersCacheSpy;
+
+ beforeEach(() => {
+ spyOn(UsersCache, 'retrieve').and.callFake(username => usersCacheSpy(username));
+ });
+
+ it('ignores special value "none"', (done) => {
+ usersCacheSpy = (username) => {
+ expect(username).toBe('none');
+ done.fail('Should not resolve "none"!');
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, 'none')
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('ignores error if UsersCache throws', (done) => {
+ spyOn(window, 'Flash');
+ const dummyError = new Error('Earth rotated backwards');
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.reject(dummyError);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(window.Flash.calls.count()).toBe(0);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does nothing if user cannot be found', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(undefined);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueElement.innerText).toBe(tokenValue);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('replaces author token with avatar and display name', (done) => {
+ const dummyUser = {
+ name: 'Important Person',
+ avatar_url: 'https://host.invalid/mypics/avatar.png',
+ };
+ const { tokenValueContainer, tokenValueElement } = findElements(authorToken);
+ const tokenValue = tokenValueElement.innerText;
+ usersCacheSpy = (username) => {
+ expect(`@${username}`).toBe(tokenValue);
+ return Promise.resolve(dummyUser);
+ };
+
+ subject.updateUserTokenAppearance(tokenValueContainer, tokenValueElement, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.dataset.originalValue).toBe(tokenValue);
+ expect(tokenValueElement.innerText.trim()).toBe(dummyUser.name);
+ const avatar = tokenValueElement.querySelector('img.avatar');
+ expect(avatar.src).toBe(dummyUser.avatar_url);
+ })
+ .then(done)
+ .catch(done.fail);
});
});
@@ -659,21 +802,16 @@ describe('Filtered Search Visual Tokens', () => {
const dummyEndpoint = '/dummy/endpoint';
preloadFixtures(jsonFixtureName);
- const labelData = getJSONFixture(jsonFixtureName);
- const findLabel = tokenValue => labelData.find(
- label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
- );
- const bugLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~bug');
+ let labelData;
+
+ beforeAll(() => {
+ labelData = getJSONFixture(jsonFixtureName);
+ });
+
const missingLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~doesnotexist');
const spaceLabelToken = FilteredSearchSpecHelper.createFilterVisualToken('label', '~"some space"');
- const parseColor = (color) => {
- const dummyElement = document.createElement('div');
- dummyElement.style.color = color;
- return dummyElement.style.color;
- };
-
beforeEach(() => {
tokensContainer.innerHTML = FilteredSearchSpecHelper.createTokensContainerHTML(`
${bugLabelToken.outerHTML}
@@ -688,28 +826,60 @@ describe('Filtered Search Visual Tokens', () => {
AjaxCache.internalStorage[`${dummyEndpoint}/labels.json`] = labelData;
});
- const testCase = (token, done) => {
- const tokenValueContainer = token.querySelector('.value-container');
- const tokenValue = token.querySelector('.value').innerText;
- const label = findLabel(tokenValue);
+ const parseColor = (color) => {
+ const dummyElement = document.createElement('div');
+ dummyElement.style.color = color;
+ return dummyElement.style.color;
+ };
- gl.FilteredSearchVisualTokens.updateLabelTokenColor(tokenValueContainer, tokenValue)
- .then(() => {
- if (label) {
- expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
- expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
- expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
- } else {
- expect(token).toBe(missingLabelToken);
- expect(tokenValueContainer.getAttribute('style')).toBe(null);
- }
- })
- .then(done)
- .catch(fail);
+ const expectValueContainerStyle = (tokenValueContainer, label) => {
+ expect(tokenValueContainer.getAttribute('style')).not.toBe(null);
+ expect(tokenValueContainer.style.backgroundColor).toBe(parseColor(label.color));
+ expect(tokenValueContainer.style.color).toBe(parseColor(label.text_color));
};
- it('updates the color of a label token', done => testCase(bugLabelToken, done));
- it('updates the color of a label token with spaces', done => testCase(spaceLabelToken, done));
- it('does not change color of a missing label', done => testCase(missingLabelToken, done));
+ const findLabel = tokenValue => labelData.find(
+ label => tokenValue === `~${gl.DropdownUtils.getEscapedText(label.title)}`,
+ );
+
+ it('updates the color of a label token', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(bugLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('updates the color of a label token with spaces', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(spaceLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expectValueContainerStyle(tokenValueContainer, matchingLabel);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
+
+ it('does not change color of a missing label', (done) => {
+ const { tokenValueContainer, tokenValueElement } = findElements(missingLabelToken);
+ const tokenValue = tokenValueElement.innerText;
+ const matchingLabel = findLabel(tokenValue);
+ expect(matchingLabel).toBe(undefined);
+
+ subject.updateLabelTokenColor(tokenValueContainer, tokenValue)
+ .then(() => {
+ expect(tokenValueContainer.getAttribute('style')).toBe(null);
+ })
+ .then(done)
+ .catch(done.fail);
+ });
});
});
diff --git a/spec/javascripts/fixtures/issues.rb b/spec/javascripts/fixtures/issues.rb
index 88e3f860809..1a30909977e 100644
--- a/spec/javascripts/fixtures/issues.rb
+++ b/spec/javascripts/fixtures/issues.rb
@@ -36,6 +36,17 @@ describe Projects::IssuesController, '(JavaScript fixtures)', type: :controller
render_issue(example.description, issue)
end
+ it 'issues/issue_list.html.raw' do |example|
+ create(:issue, project: project)
+
+ get :index,
+ namespace_id: project.namespace.to_param,
+ project_id: project
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+
private
def render_issue(fixture_file_name, issue)
diff --git a/spec/javascripts/fixtures/raw.rb b/spec/javascripts/fixtures/raw.rb
index 1ce622fc836..17533443d76 100644
--- a/spec/javascripts/fixtures/raw.rb
+++ b/spec/javascripts/fixtures/raw.rb
@@ -21,4 +21,10 @@ describe 'Raw files', '(JavaScript fixtures)', type: :controller do
store_frontend_fixture(blob.data, example.description)
end
+
+ it 'blob/notebook/math.json' do |example|
+ blob = project.repository.blob_at('93ee732', 'files/ipython/math.ipynb')
+
+ store_frontend_fixture(blob.data, example.description)
+ end
end
diff --git a/spec/javascripts/fixtures/services.rb b/spec/javascripts/fixtures/services.rb
new file mode 100644
index 00000000000..554451d1bbf
--- /dev/null
+++ b/spec/javascripts/fixtures/services.rb
@@ -0,0 +1,31 @@
+require 'spec_helper'
+
+describe Projects::ServicesController, '(JavaScript fixtures)', type: :controller do
+ include JavaScriptFixturesHelpers
+
+ let(:admin) { create(:admin) }
+ let(:namespace) { create(:namespace, name: 'frontend-fixtures' )}
+ let(:project) { create(:project_empty_repo, namespace: namespace, path: 'services-project') }
+ let!(:service) { create(:custom_issue_tracker_service, project: project, title: 'Custom Issue Tracker') }
+
+
+ render_views
+
+ before(:all) do
+ clean_frontend_fixtures('services/')
+ end
+
+ before(:each) do
+ sign_in(admin)
+ end
+
+ it 'services/edit_service.html.raw' do |example|
+ get :edit,
+ namespace_id: namespace,
+ project_id: project,
+ id: service.to_param
+
+ expect(response).to be_success
+ store_frontend_fixture(response, example.description)
+ end
+end
diff --git a/spec/javascripts/helpers/filtered_search_spec_helper.js b/spec/javascripts/helpers/filtered_search_spec_helper.js
index 0d7092a2357..8933dd5def4 100644
--- a/spec/javascripts/helpers/filtered_search_spec_helper.js
+++ b/spec/javascripts/helpers/filtered_search_spec_helper.js
@@ -30,12 +30,15 @@ export default class FilteredSearchSpecHelper {
`;
}
+ static createSearchVisualToken(name) {
+ const li = document.createElement('li');
+ li.classList.add('js-visual-token', 'filtered-search-term');
+ li.innerHTML = `<div class="name">${name}</div>`;
+ return li;
+ }
+
static createSearchVisualTokenHTML(name) {
- return `
- <li class="js-visual-token filtered-search-term">
- <div class="name">${name}</div>
- </li>
- `;
+ return FilteredSearchSpecHelper.createSearchVisualToken(name).outerHTML;
}
static createInputHTML(placeholder = '', value = '') {
diff --git a/spec/javascripts/integrations/integration_settings_form_spec.js b/spec/javascripts/integrations/integration_settings_form_spec.js
new file mode 100644
index 00000000000..45909d4e70e
--- /dev/null
+++ b/spec/javascripts/integrations/integration_settings_form_spec.js
@@ -0,0 +1,199 @@
+import IntegrationSettingsForm from '~/integrations/integration_settings_form';
+
+describe('IntegrationSettingsForm', () => {
+ const FIXTURE = 'services/edit_service.html.raw';
+ preloadFixtures(FIXTURE);
+
+ beforeEach(() => {
+ loadFixtures(FIXTURE);
+ });
+
+ describe('contructor', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ spyOn(integrationSettingsForm, 'init');
+ });
+
+ it('should initialize form element refs on class object', () => {
+ // Form Reference
+ expect(integrationSettingsForm.$form).toBeDefined();
+ expect(integrationSettingsForm.$form.prop('nodeName')).toEqual('FORM');
+
+ // Form Child Elements
+ expect(integrationSettingsForm.$serviceToggle).toBeDefined();
+ expect(integrationSettingsForm.$submitBtn).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLoader).toBeDefined();
+ expect(integrationSettingsForm.$submitBtnLabel).toBeDefined();
+ });
+
+ it('should initialize form metadata on class object', () => {
+ expect(integrationSettingsForm.testEndPoint).toBeDefined();
+ expect(integrationSettingsForm.canTestService).toBeDefined();
+ });
+ });
+
+ describe('toggleServiceState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should remove `novalidate` attribute to form when called with `true`', () => {
+ integrationSettingsForm.toggleServiceState(true);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).not.toBeDefined();
+ });
+
+ it('should set `novalidate` attribute to form when called with `false`', () => {
+ integrationSettingsForm.toggleServiceState(false);
+
+ expect(integrationSettingsForm.$form.attr('novalidate')).toBeDefined();
+ });
+ });
+
+ describe('toggleSubmitBtnLabel', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should set Save button label to "Test settings and save changes" when serviceActive & canTestService are `true`', () => {
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Test settings and save changes');
+ });
+
+ it('should set Save button label to "Save changes" when either serviceActive or canTestService (or both) is `false`', () => {
+ integrationSettingsForm.canTestService = false;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.toggleSubmitBtnLabel(true);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+
+ integrationSettingsForm.canTestService = true;
+
+ integrationSettingsForm.toggleSubmitBtnLabel(false);
+ expect(integrationSettingsForm.$submitBtnLabel.text()).toEqual('Save changes');
+ });
+ });
+
+ describe('toggleSubmitBtnState', () => {
+ let integrationSettingsForm;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ });
+
+ it('should disable Save button and show loader animation when called with `true`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(true);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeTruthy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeFalsy();
+ });
+
+ it('should enable Save button and hide loader animation when called with `false`', () => {
+ integrationSettingsForm.toggleSubmitBtnState(false);
+
+ expect(integrationSettingsForm.$submitBtn.is(':disabled')).toBeFalsy();
+ expect(integrationSettingsForm.$submitBtnLoader.hasClass('hidden')).toBeTruthy();
+ });
+ });
+
+ describe('testSettings', () => {
+ let integrationSettingsForm;
+ let formData;
+
+ beforeEach(() => {
+ integrationSettingsForm = new IntegrationSettingsForm('.js-integration-settings-form');
+ formData = integrationSettingsForm.$form.serialize();
+ });
+
+ it('should make an ajax request with provided `formData`', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ expect($.ajax).toHaveBeenCalledWith({
+ type: 'PUT',
+ url: integrationSettingsForm.testEndPoint,
+ data: formData,
+ });
+ });
+
+ it('should show error Flash with `Save anyway` action if ajax request responds with error in test', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashContainer = $('.flash-container');
+ expect($flashContainer.find('.flash-text').text()).toEqual(errorMessage);
+ expect($flashContainer.find('.flash-action')).toBeDefined();
+ expect($flashContainer.find('.flash-action').text()).toEqual('Save anyway');
+ });
+
+ it('should submit form if ajax request responds without any error in test', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ deferred.resolve({ error: false });
+
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should submit form when clicked on `Save anyway` action of error Flash', () => {
+ const errorMessage = 'Test failed.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.resolve({ error: true, message: errorMessage });
+
+ const $flashAction = $('.flash-container .flash-action');
+ expect($flashAction).toBeDefined();
+
+ spyOn(integrationSettingsForm.$form, 'submit');
+ $flashAction.trigger('click');
+ expect(integrationSettingsForm.$form.submit).toHaveBeenCalled();
+ });
+
+ it('should show error Flash if ajax request failed', () => {
+ const errorMessage = 'Something went wrong on our end.';
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ deferred.reject();
+
+ expect($('.flash-container .flash-text').text()).toEqual(errorMessage);
+ });
+
+ it('should always call `toggleSubmitBtnState` with `false` once request is completed', () => {
+ const deferred = $.Deferred();
+ spyOn($, 'ajax').and.returnValue(deferred.promise());
+
+ integrationSettingsForm.testSettings(formData);
+
+ spyOn(integrationSettingsForm, 'toggleSubmitBtnState');
+ deferred.reject();
+
+ expect(integrationSettingsForm.toggleSubmitBtnState).toHaveBeenCalledWith(false);
+ });
+ });
+});
diff --git a/spec/javascripts/notebook/cells/markdown_spec.js b/spec/javascripts/notebook/cells/markdown_spec.js
index 38c976f38d8..a88e9ed3d99 100644
--- a/spec/javascripts/notebook/cells/markdown_spec.js
+++ b/spec/javascripts/notebook/cells/markdown_spec.js
@@ -1,8 +1,11 @@
import Vue from 'vue';
import MarkdownComponent from '~/notebook/cells/markdown.vue';
+import katex from 'vendor/katex';
const Component = Vue.extend(MarkdownComponent);
+window.katex = katex;
+
describe('Markdown component', () => {
let vm;
let cell;
@@ -38,4 +41,58 @@ describe('Markdown component', () => {
it('renders the markdown HTML', () => {
expect(vm.$el.querySelector('.markdown h1')).not.toBeNull();
});
+
+ describe('katex', () => {
+ beforeEach(() => {
+ json = getJSONFixture('blob/notebook/math.json');
+ });
+
+ it('renders multi-line katex', (done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[0],
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('.katex'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('renders inline katex', (done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[1],
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelector('p:first-child .katex'),
+ ).not.toBeNull();
+
+ done();
+ });
+ });
+
+ it('renders multiple inline katex', (done) => {
+ vm = new Component({
+ propsData: {
+ cell: json.cells[1],
+ },
+ }).$mount();
+
+ Vue.nextTick(() => {
+ expect(
+ vm.$el.querySelectorAll('p:nth-child(2) .katex').length,
+ ).toBe(4);
+
+ done();
+ });
+ });
+ });
});
diff --git a/spec/javascripts/pipelines/header_component_spec.js b/spec/javascripts/pipelines/header_component_spec.js
new file mode 100644
index 00000000000..b8531350e43
--- /dev/null
+++ b/spec/javascripts/pipelines/header_component_spec.js
@@ -0,0 +1,60 @@
+import Vue from 'vue';
+import headerComponent from '~/pipelines/components/header_component.vue';
+import eventHub from '~/pipelines/event_hub';
+
+describe('Pipeline details header', () => {
+ let HeaderComponent;
+ let vm;
+ let props;
+
+ beforeEach(() => {
+ HeaderComponent = Vue.extend(headerComponent);
+
+ props = {
+ pipeline: {
+ details: {
+ status: {
+ group: 'failed',
+ icon: 'ci-status-failed',
+ label: 'failed',
+ text: 'failed',
+ details_path: 'path',
+ },
+ },
+ id: 123,
+ created_at: '2017-05-08T14:57:39.781Z',
+ user: {
+ web_url: 'path',
+ name: 'Foo',
+ username: 'foobar',
+ email: 'foo@bar.com',
+ avatar_url: 'link',
+ },
+ retry_path: 'path',
+ },
+ isLoading: false,
+ };
+
+ vm = new HeaderComponent({ propsData: props }).$mount();
+ });
+
+ afterEach(() => {
+ vm.$destroy();
+ });
+
+ it('should render provided pipeline info', () => {
+ expect(
+ vm.$el.querySelector('.header-main-content').textContent.replace(/\s+/g, ' ').trim(),
+ ).toEqual('failed Pipeline #123 triggered 3 weeks ago by Foo');
+ });
+
+ describe('action buttons', () => {
+ it('should call postAction when button action is clicked', () => {
+ eventHub.$on('headerPostAction', (action) => {
+ expect(action.path).toEqual('path');
+ });
+
+ vm.$el.querySelector('button').click();
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_details_mediator_spec.js b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
new file mode 100644
index 00000000000..9fec2f61f78
--- /dev/null
+++ b/spec/javascripts/pipelines/pipeline_details_mediator_spec.js
@@ -0,0 +1,41 @@
+import Vue from 'vue';
+import PipelineMediator from '~/pipelines/pipeline_details_mediatior';
+
+describe('PipelineMdediator', () => {
+ let mediator;
+ beforeEach(() => {
+ mediator = new PipelineMediator({ endpoint: 'foo' });
+ });
+
+ it('should set defaults', () => {
+ expect(mediator.options).toEqual({ endpoint: 'foo' });
+ expect(mediator.state.isLoading).toEqual(false);
+ expect(mediator.store).toBeDefined();
+ expect(mediator.service).toBeDefined();
+ });
+
+ describe('request and store data', () => {
+ const interceptor = (request, next) => {
+ next(request.respondWith(JSON.stringify({ foo: 'bar' }), {
+ status: 200,
+ }));
+ };
+
+ beforeEach(() => {
+ Vue.http.interceptors.push(interceptor);
+ });
+
+ afterEach(() => {
+ Vue.http.interceptors = _.without(Vue.http.interceptor, interceptor);
+ });
+
+ it('should store received data', (done) => {
+ mediator.fetchPipeline();
+
+ setTimeout(() => {
+ expect(mediator.store.state.pipeline).toEqual({ foo: 'bar' });
+ done();
+ });
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_store_spec.js b/spec/javascripts/pipelines/pipeline_store_spec.js
new file mode 100644
index 00000000000..85d13445b01
--- /dev/null
+++ b/spec/javascripts/pipelines/pipeline_store_spec.js
@@ -0,0 +1,27 @@
+import PipelineStore from '~/pipelines/stores/pipeline_store';
+
+describe('Pipeline Store', () => {
+ let store;
+
+ beforeEach(() => {
+ store = new PipelineStore();
+ });
+
+ it('should set defaults', () => {
+ expect(store.state).toEqual({ pipeline: {} });
+ expect(store.state.pipeline).toEqual({});
+ });
+
+ describe('storePipeline', () => {
+ it('should store empty object if none is provided', () => {
+ store.storePipeline();
+
+ expect(store.state.pipeline).toEqual({});
+ });
+
+ it('should store received object', () => {
+ store.storePipeline({ foo: 'bar' });
+ expect(store.state.pipeline).toEqual({ foo: 'bar' });
+ });
+ });
+});
diff --git a/spec/javascripts/pipelines/pipeline_url_spec.js b/spec/javascripts/pipelines/pipeline_url_spec.js
index d74b1281668..594a9856d2c 100644
--- a/spec/javascripts/pipelines/pipeline_url_spec.js
+++ b/spec/javascripts/pipelines/pipeline_url_spec.js
@@ -47,6 +47,7 @@ describe('Pipeline Url Component', () => {
web_url: '/',
name: 'foo',
avatar_url: '/',
+ path: '/',
},
},
};
diff --git a/spec/javascripts/vue_shared/components/commit_spec.js b/spec/javascripts/vue_shared/components/commit_spec.js
index 0638483e7aa..050170a54e9 100644
--- a/spec/javascripts/vue_shared/components/commit_spec.js
+++ b/spec/javascripts/vue_shared/components/commit_spec.js
@@ -24,6 +24,7 @@ describe('Commit component', () => {
author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
+ path: '/jschatz1',
username: 'jschatz1',
},
},
@@ -46,6 +47,7 @@ describe('Commit component', () => {
author: {
avatar_url: 'https://gitlab.com/uploads/user/avatar/300478/avatar.png',
web_url: 'https://gitlab.com/jschatz1',
+ path: '/jschatz1',
username: 'jschatz1',
},
commitIconSvg: '<svg></svg>',
@@ -81,7 +83,7 @@ describe('Commit component', () => {
it('should render a link to the author profile', () => {
expect(
component.$el.querySelector('.commit-title .avatar-image-container').getAttribute('href'),
- ).toEqual(props.author.web_url);
+ ).toEqual(props.author.path);
});
it('Should render the author avatar with title and alt attributes', () => {
diff --git a/spec/javascripts/vue_shared/components/header_ci_component_spec.js b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
index 1bf8916b3d0..2b51c89f311 100644
--- a/spec/javascripts/vue_shared/components/header_ci_component_spec.js
+++ b/spec/javascripts/vue_shared/components/header_ci_component_spec.js
@@ -33,12 +33,14 @@ describe('Header CI Component', () => {
path: 'path',
type: 'button',
cssClass: 'btn',
+ isLoading: false,
},
{
label: 'Go',
path: 'path',
type: 'link',
cssClass: 'link',
+ isLoading: false,
},
],
};
@@ -79,4 +81,13 @@ describe('Header CI Component', () => {
expect(vm.$el.querySelector('.link').textContent.trim()).toEqual(props.actions[1].label);
expect(vm.$el.querySelector('.link').getAttribute('href')).toEqual(props.actions[0].path);
});
+
+ it('should show loading icon', (done) => {
+ vm.actions[0].isLoading = true;
+
+ Vue.nextTick(() => {
+ expect(vm.$el.querySelector('.btn .fa-spinner').getAttribute('style')).toEqual('');
+ done();
+ });
+ });
});
diff --git a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
index 286118917e8..67419cfcbea 100644
--- a/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
+++ b/spec/javascripts/vue_shared/components/pipelines_table_row_spec.js
@@ -76,7 +76,7 @@ describe('Pipelines Table Row', () => {
it('should render user information', () => {
expect(
component.$el.querySelector('td:nth-child(2) a:nth-child(3)').getAttribute('href'),
- ).toEqual(pipeline.user.web_url);
+ ).toEqual(pipeline.user.path);
expect(
component.$el.querySelector('td:nth-child(2) img').getAttribute('data-original-title'),
@@ -120,7 +120,7 @@ describe('Pipelines Table Row', () => {
component = buildComponent(pipeline);
const { commitAuthorLink, commitAuthorName } = findElements();
- expect(commitAuthorLink).toEqual(pipeline.commit.author.web_url);
+ expect(commitAuthorLink).toEqual(pipeline.commit.author.path);
expect(commitAuthorName).toEqual(pipeline.commit.author.username);
});
diff --git a/spec/lib/banzai/filter/user_reference_filter_spec.rb b/spec/lib/banzai/filter/user_reference_filter_spec.rb
index 63b23dac7ed..edf3846b742 100644
--- a/spec/lib/banzai/filter/user_reference_filter_spec.rb
+++ b/spec/lib/banzai/filter/user_reference_filter_spec.rb
@@ -16,6 +16,11 @@ describe Banzai::Filter::UserReferenceFilter, lib: true do
expect(reference_filter(act).to_html).to eq(exp)
end
+ it 'ignores references with text before the @ sign' do
+ exp = act = "Hey foo#{reference}"
+ expect(reference_filter(act).to_html).to eq(exp)
+ end
+
%w(pre code a style).each do |elem|
it "ignores valid references contained inside '#{elem}' element" do
exp = act = "<#{elem}>Hey #{reference}</#{elem}>"
diff --git a/spec/lib/gitlab/git/encoding_helper_spec.rb b/spec/lib/gitlab/encoding_helper_spec.rb
index 48fc817d857..1482ef7132d 100644
--- a/spec/lib/gitlab/git/encoding_helper_spec.rb
+++ b/spec/lib/gitlab/encoding_helper_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
-describe Gitlab::Git::EncodingHelper do
- let(:ext_class) { Class.new { extend Gitlab::Git::EncodingHelper } }
+describe Gitlab::EncodingHelper do
+ let(:ext_class) { Class.new { extend Gitlab::EncodingHelper } }
let(:binary_string) { File.read(Rails.root + "spec/fixtures/dk.png") }
describe '#encode!' do
diff --git a/spec/lib/gitlab/etag_caching/router_spec.rb b/spec/lib/gitlab/etag_caching/router_spec.rb
index 22494f4dcf5..269798c7c9e 100644
--- a/spec/lib/gitlab/etag_caching/router_spec.rb
+++ b/spec/lib/gitlab/etag_caching/router_spec.rb
@@ -88,6 +88,17 @@ describe Gitlab::EtagCaching::Router do
expect(result).to be_blank
end
+ it 'matches the environments path' do
+ env = build_env(
+ '/my-group/my-project/environments.json'
+ )
+
+ result = described_class.match(env)
+ expect(result).to be_present
+
+ expect(result.name).to eq 'environments'
+ end
+
it 'matches pipeline#show endpoint' do
env = build_env(
'/my-group/my-project/pipelines/2.json'
diff --git a/spec/lib/gitlab/git/diff_collection_spec.rb b/spec/lib/gitlab/git/diff_collection_spec.rb
index ae617b313c5..3565e719ad3 100644
--- a/spec/lib/gitlab/git/diff_collection_spec.rb
+++ b/spec/lib/gitlab/git/diff_collection_spec.rb
@@ -6,8 +6,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
iterator,
max_files: max_files,
max_lines: max_lines,
- all_diffs: all_diffs,
- no_collapse: no_collapse
+ limits: limits,
+ expanded: expanded
)
end
let(:iterator) { MutatingConstantIterator.new(file_count, fake_diff(line_length, line_count)) }
@@ -16,8 +16,8 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
let(:line_count) { 1 }
let(:max_files) { 10 }
let(:max_lines) { 100 }
- let(:all_diffs) { false }
- let(:no_collapse) { true }
+ let(:limits) { true }
+ let(:expanded) { true }
describe '#to_a' do
subject { super().to_a }
@@ -75,7 +75,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
describe '#overflow?' do
subject { super().overflow? }
@@ -94,7 +94,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
describe '#size' do
it { expect(subject.size).to eq(3) }
-
+
it 'does not change after peeking' do
subject.any?
expect(subject.size).to eq(3)
@@ -123,7 +123,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { expect(subject.size).to eq(0) }
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
describe '#overflow?' do
subject { super().overflow? }
@@ -167,7 +167,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { expect(subject.size).to eq(10) }
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
describe '#overflow?' do
subject { super().overflow? }
@@ -207,7 +207,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { expect(subject.size).to eq(3) }
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
describe '#overflow?' do
subject { super().overflow? }
@@ -273,7 +273,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
it { expect(subject.size).to eq(9) }
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
describe '#overflow?' do
subject { super().overflow? }
@@ -344,7 +344,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
let(:iterator) { [{ diff: 'a' * 20480 }] }
context 'when no collapse is set' do
- let(:no_collapse) { true }
+ let(:expanded) { true }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
@@ -363,7 +363,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when no collapse is unset' do
- let(:no_collapse) { false }
+ let(:expanded) { false }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
@@ -450,7 +450,7 @@ describe Gitlab::Git::DiffCollection, seed_helper: true do
end
context 'when limiting is disabled' do
- let(:all_diffs) { true }
+ let(:limits) { false }
it 'yields Diff instances even when they are quite big' do
expect { |b| subject.each(&b) }.
diff --git a/spec/lib/gitlab/git/diff_spec.rb b/spec/lib/gitlab/git/diff_spec.rb
index 4189aaef643..8e24168ad71 100644
--- a/spec/lib/gitlab/git/diff_spec.rb
+++ b/spec/lib/gitlab/git/diff_spec.rb
@@ -85,12 +85,12 @@ EOT
# The patch total size is 200, with lines between 21 and 54.
# This is a quick-and-dirty way to test this. Ideally, a new patch is
# added to the test repo with a size that falls between the real limits.
- stub_const("#{described_class}::DIFF_SIZE_LIMIT", 150)
- stub_const("#{described_class}::DIFF_COLLAPSE_LIMIT", 100)
+ stub_const("#{described_class}::SIZE_LIMIT", 150)
+ stub_const("#{described_class}::COLLAPSE_LIMIT", 100)
end
it 'prunes the diff as a large diff instead of as a collapsed diff' do
- diff = described_class.new(@rugged_diff, collapse: true)
+ diff = described_class.new(@rugged_diff, expanded: false)
expect(diff.diff).to be_empty
expect(diff).to be_too_large
@@ -269,7 +269,7 @@ EOT
it 'returns true for a diff that was explicitly marked as being too large' do
diff = described_class.new(diff: 'a')
- diff.prune_large_diff!
+ diff.too_large!
expect(diff.too_large?).to eq(true)
end
@@ -291,31 +291,31 @@ EOT
it 'returns true for a diff that was explicitly marked as being collapsed' do
diff = described_class.new(diff: 'a')
- diff.prune_collapsed_diff!
+ diff.collapse!
expect(diff).to be_collapsed
end
end
- describe '#collapsible?' do
+ describe '#collapsed?' do
it 'returns true for a diff that is quite large' do
- diff = described_class.new(diff: 'a' * 20480)
+ diff = described_class.new({ diff: 'a' * 20480 }, expanded: false)
- expect(diff).to be_collapsible
+ expect(diff).to be_collapsed
end
it 'returns false for a diff that is small enough' do
- diff = described_class.new(diff: 'a')
+ diff = described_class.new({ diff: 'a' }, expanded: false)
- expect(diff).not_to be_collapsible
+ expect(diff).not_to be_collapsed
end
end
- describe '#prune_collapsed_diff!' do
+ describe '#collapse!' do
it 'prunes the diff' do
diff = described_class.new(diff: "foo\nbar")
- diff.prune_collapsed_diff!
+ diff.collapse!
expect(diff.diff).to eq('')
expect(diff.line_count).to eq(0)
diff --git a/spec/lib/gitlab/git/repository_spec.rb b/spec/lib/gitlab/git/repository_spec.rb
index 9d0e95d5b19..26215381cc4 100644
--- a/spec/lib/gitlab/git/repository_spec.rb
+++ b/spec/lib/gitlab/git/repository_spec.rb
@@ -1,7 +1,7 @@
require "spec_helper"
describe Gitlab::Git::Repository, seed_helper: true do
- include Gitlab::Git::EncodingHelper
+ include Gitlab::EncodingHelper
let(:repository) { Gitlab::Git::Repository.new('default', TEST_REPO_PATH) }
diff --git a/spec/lib/gitlab/utils_spec.rb b/spec/lib/gitlab/utils_spec.rb
index 56772409989..00941aec380 100644
--- a/spec/lib/gitlab/utils_spec.rb
+++ b/spec/lib/gitlab/utils_spec.rb
@@ -1,5 +1,7 @@
+require 'spec_helper'
+
describe Gitlab::Utils, lib: true do
- delegate :to_boolean, to: :described_class
+ delegate :to_boolean, :boolean_to_yes_no, to: :described_class
describe '.to_boolean' do
it 'accepts booleans' do
@@ -30,4 +32,11 @@ describe Gitlab::Utils, lib: true do
expect(to_boolean(nil)).to be_nil
end
end
+
+ describe '.boolean_to_yes_no' do
+ it 'converts booleans to Yes or No' do
+ expect(boolean_to_yes_no(true)).to eq('Yes')
+ expect(boolean_to_yes_no(false)).to eq('No')
+ end
+ end
end
diff --git a/spec/lib/system_check/simple_executor_spec.rb b/spec/lib/system_check/simple_executor_spec.rb
new file mode 100644
index 00000000000..a5c6170cd7d
--- /dev/null
+++ b/spec/lib/system_check/simple_executor_spec.rb
@@ -0,0 +1,223 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck::SimpleExecutor, lib: true do
+ class SimpleCheck < SystemCheck::BaseCheck
+ set_name 'my simple check'
+
+ def check?
+ true
+ end
+ end
+
+ class OtherCheck < SystemCheck::BaseCheck
+ set_name 'other check'
+
+ def check?
+ false
+ end
+
+ def show_error
+ $stdout.puts 'this is an error text'
+ end
+ end
+
+ class SkipCheck < SystemCheck::BaseCheck
+ set_name 'skip check'
+ set_skip_reason 'this is a skip reason'
+
+ def skip?
+ true
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
+ class MultiCheck < SystemCheck::BaseCheck
+ set_name 'multi check'
+
+ def multi_check
+ $stdout.puts 'this is a multi output check'
+ end
+
+ def check?
+ raise 'should not execute this'
+ end
+ end
+
+ class SkipMultiCheck < SystemCheck::BaseCheck
+ set_name 'skip multi check'
+
+ def skip?
+ true
+ end
+
+ def multi_check
+ raise 'should not execute this'
+ end
+ end
+
+ class RepairCheck < SystemCheck::BaseCheck
+ set_name 'repair check'
+
+ def check?
+ false
+ end
+
+ def repair!
+ true
+ end
+
+ def show_error
+ $stdout.puts 'this is an error message'
+ end
+ end
+
+ describe '#component' do
+ it 'returns stored component name' do
+ expect(subject.component).to eq('Test')
+ end
+ end
+
+ describe '#checks' do
+ before do
+ subject << SimpleCheck
+ end
+
+ it 'returns a set of classes' do
+ expect(subject.checks).to include(SimpleCheck)
+ end
+ end
+
+ describe '#<<' do
+ before do
+ subject << SimpleCheck
+ end
+
+ it 'appends a new check to the Set' do
+ subject << OtherCheck
+ stored_checks = subject.checks.to_a
+
+ expect(stored_checks.first).to eq(SimpleCheck)
+ expect(stored_checks.last).to eq(OtherCheck)
+ end
+
+ it 'inserts unique itens only' do
+ subject << SimpleCheck
+
+ expect(subject.checks.size).to eq(1)
+ end
+ end
+
+ subject { described_class.new('Test') }
+
+ describe '#execute' do
+ before do
+ silence_output
+
+ subject << SimpleCheck
+ subject << OtherCheck
+ end
+
+ it 'runs included checks' do
+ expect(subject).to receive(:run_check).with(SimpleCheck)
+ expect(subject).to receive(:run_check).with(OtherCheck)
+
+ subject.execute
+ end
+ end
+
+ describe '#run_check' do
+ it 'prints check name' do
+ expect(SimpleCheck).to receive(:display_name).and_call_original
+ expect { subject.run_check(SimpleCheck) }.to output(/my simple check/).to_stdout
+ end
+
+ context 'when check pass' do
+ it 'prints yes' do
+ expect_any_instance_of(SimpleCheck).to receive(:check?).and_call_original
+ expect { subject.run_check(SimpleCheck) }.to output(/ \.\.\. yes/).to_stdout
+ end
+ end
+
+ context 'when check fails' do
+ it 'prints no' do
+ expect_any_instance_of(OtherCheck).to receive(:check?).and_call_original
+ expect { subject.run_check(OtherCheck) }.to output(/ \.\.\. no/).to_stdout
+ end
+
+ it 'displays error message from #show_error' do
+ expect_any_instance_of(OtherCheck).to receive(:show_error).and_call_original
+ expect { subject.run_check(OtherCheck) }.to output(/this is an error text/).to_stdout
+ end
+
+ context 'when check implements #repair!' do
+ it 'executes #repair!' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!)
+
+ subject.run_check(RepairCheck)
+ end
+
+ context 'when repair succeeds' do
+ it 'does not execute #show_error' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!).and_call_original
+ expect_any_instance_of(RepairCheck).not_to receive(:show_error)
+
+ subject.run_check(RepairCheck)
+ end
+ end
+
+ context 'when repair fails' do
+ it 'does not execute #show_error' do
+ expect_any_instance_of(RepairCheck).to receive(:repair!) { false }
+ expect_any_instance_of(RepairCheck).to receive(:show_error)
+
+ subject.run_check(RepairCheck)
+ end
+ end
+ end
+ end
+
+ context 'when check implements skip?' do
+ it 'executes #skip? method' do
+ expect_any_instance_of(SkipCheck).to receive(:skip?).and_call_original
+
+ subject.run_check(SkipCheck)
+ end
+
+ it 'displays #skip_reason' do
+ expect { subject.run_check(SkipCheck) }.to output(/this is a skip reason/).to_stdout
+ end
+
+ it 'does not execute #check when #skip? is true' do
+ expect_any_instance_of(SkipCheck).not_to receive(:check?)
+
+ subject.run_check(SkipCheck)
+ end
+ end
+
+ context 'when implements a #multi_check' do
+ it 'executes #multi_check method' do
+ expect_any_instance_of(MultiCheck).to receive(:multi_check)
+
+ subject.run_check(MultiCheck)
+ end
+
+ it 'does not execute #check method' do
+ expect_any_instance_of(MultiCheck).not_to receive(:check)
+
+ subject.run_check(MultiCheck)
+ end
+
+ context 'when check implements #skip?' do
+ it 'executes #skip? method' do
+ expect_any_instance_of(SkipMultiCheck).to receive(:skip?).and_call_original
+
+ subject.run_check(SkipMultiCheck)
+ end
+ end
+ end
+ end
+end
diff --git a/spec/lib/system_check_spec.rb b/spec/lib/system_check_spec.rb
new file mode 100644
index 00000000000..23d9beddb08
--- /dev/null
+++ b/spec/lib/system_check_spec.rb
@@ -0,0 +1,36 @@
+require 'spec_helper'
+require 'rake_helper'
+
+describe SystemCheck, lib: true do
+ class SimpleCheck < SystemCheck::BaseCheck
+ def check?
+ true
+ end
+ end
+
+ class OtherCheck < SystemCheck::BaseCheck
+ def check?
+ false
+ end
+ end
+
+ before do
+ silence_output
+ end
+
+ describe '.run' do
+ subject { SystemCheck }
+
+ it 'detects execution of SimpleCheck' do
+ is_expected.to execute_check(SimpleCheck)
+
+ subject.run('Test', [SimpleCheck])
+ end
+
+ it 'detects exclusion of OtherCheck in execution' do
+ is_expected.not_to execute_check(OtherCheck)
+
+ subject.run('Test', [SimpleCheck])
+ end
+ end
+end
diff --git a/spec/migrations/migrate_old_artifacts_spec.rb b/spec/migrations/migrate_old_artifacts_spec.rb
new file mode 100644
index 00000000000..50f4bbda001
--- /dev/null
+++ b/spec/migrations/migrate_old_artifacts_spec.rb
@@ -0,0 +1,117 @@
+# encoding: utf-8
+
+require 'spec_helper'
+require Rails.root.join('db', 'post_migrate', '20170523083112_migrate_old_artifacts.rb')
+
+describe MigrateOldArtifacts do
+ let(:migration) { described_class.new }
+ let!(:directory) { Dir.mktmpdir }
+
+ before do
+ allow(Gitlab.config.artifacts).to receive(:path).and_return(directory)
+ end
+
+ after do
+ FileUtils.remove_entry_secure(directory)
+ end
+
+ context 'with migratable data' do
+ let(:project1) { create(:empty_project, ci_id: 2) }
+ let(:project2) { create(:empty_project, ci_id: 3) }
+ let(:project3) { create(:empty_project) }
+
+ let(:pipeline1) { create(:ci_empty_pipeline, project: project1) }
+ let(:pipeline2) { create(:ci_empty_pipeline, project: project2) }
+ let(:pipeline3) { create(:ci_empty_pipeline, project: project3) }
+
+ let!(:build_with_legacy_artifacts) { create(:ci_build, pipeline: pipeline1) }
+ let!(:build_without_artifacts) { create(:ci_build, pipeline: pipeline1) }
+ let!(:build2) { create(:ci_build, :artifacts, pipeline: pipeline2) }
+ let!(:build3) { create(:ci_build, :artifacts, pipeline: pipeline3) }
+
+ before do
+ store_artifacts_in_legacy_path(build_with_legacy_artifacts)
+ end
+
+ it "legacy artifacts are not accessible" do
+ expect(build_with_legacy_artifacts.artifacts?).to be_falsey
+ end
+
+ it "legacy artifacts are set" do
+ expect(build_with_legacy_artifacts.artifacts_file_identifier).not_to be_nil
+ end
+
+ describe '#min_id' do
+ subject { migration.send(:min_id) }
+
+ it 'returns the newest build for which ci_id is not defined' do
+ is_expected.to eq(build3.id)
+ end
+ end
+
+ describe '#builds_with_artifacts' do
+ subject { migration.send(:builds_with_artifacts).map(&:id) }
+
+ it 'returns a list of builds that has artifacts and could be migrated' do
+ is_expected.to contain_exactly(build_with_legacy_artifacts.id, build2.id)
+ end
+ end
+
+ describe '#up' do
+ context 'when migrating artifacts' do
+ before do
+ migration.up
+ end
+
+ it 'all files do have artifacts' do
+ Ci::Build.with_artifacts do |build|
+ expect(build).to have_artifacts
+ end
+ end
+
+ it 'artifacts are no longer present on legacy path' do
+ expect(File.exist?(legacy_path(build_with_legacy_artifacts))).to eq(false)
+ end
+ end
+
+ context 'when there are aritfacts in old and new directory' do
+ before do
+ store_artifacts_in_legacy_path(build2)
+
+ migration.up
+ end
+
+ it 'does not move old files' do
+ expect(File.exist?(legacy_path(build2))).to eq(true)
+ end
+ end
+ end
+
+ private
+
+ def store_artifacts_in_legacy_path(build)
+ FileUtils.mkdir_p(legacy_path(build))
+
+ FileUtils.copy(
+ Rails.root.join('spec/fixtures/ci_build_artifacts.zip'),
+ File.join(legacy_path(build), "ci_build_artifacts.zip"))
+
+ FileUtils.copy(
+ Rails.root.join('spec/fixtures/ci_build_artifacts_metadata.gz'),
+ File.join(legacy_path(build), "ci_build_artifacts_metadata.gz"))
+
+ build.update_columns(
+ artifacts_file: 'ci_build_artifacts.zip',
+ artifacts_metadata: 'ci_build_artifacts_metadata.gz')
+
+ build.reload
+ end
+
+ def legacy_path(build)
+ File.join(directory,
+ build.created_at.utc.strftime('%Y_%m'),
+ build.project.ci_id.to_s,
+ build.id.to_s)
+ end
+ end
+end
diff --git a/spec/models/blob_viewer/base_spec.rb b/spec/models/blob_viewer/base_spec.rb
index 92fbf64a6b7..d56379eb59d 100644
--- a/spec/models/blob_viewer/base_spec.rb
+++ b/spec/models/blob_viewer/base_spec.rb
@@ -11,8 +11,8 @@ describe BlobViewer::Base, model: true do
self.extensions = %w(pdf)
self.binary = true
- self.overridable_max_size = 1.megabyte
- self.max_size = 5.megabytes
+ self.collapse_limit = 1.megabyte
+ self.size_limit = 5.megabytes
end
end
@@ -69,77 +69,49 @@ describe BlobViewer::Base, model: true do
end
end
- describe '#exceeds_overridable_max_size?' do
- context 'when the blob size is larger than the overridable max size' do
+ describe '#collapsed?' do
+ context 'when the blob size is larger than the collapse limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns true' do
- expect(viewer.exceeds_overridable_max_size?).to be_truthy
+ expect(viewer.collapsed?).to be_truthy
end
end
- context 'when the blob size is smaller than the overridable max size' do
+ context 'when the blob size is smaller than the collapse limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns false' do
- expect(viewer.exceeds_overridable_max_size?).to be_falsey
+ expect(viewer.collapsed?).to be_falsey
end
end
end
- describe '#exceeds_max_size?' do
- context 'when the blob size is larger than the max size' do
+ describe '#too_large?' do
+ context 'when the blob size is larger than the size limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns true' do
- expect(viewer.exceeds_max_size?).to be_truthy
+ expect(viewer.too_large?).to be_truthy
end
end
- context 'when the blob size is smaller than the max size' do
+ context 'when the blob size is smaller than the size limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns false' do
- expect(viewer.exceeds_max_size?).to be_falsey
- end
- end
- end
-
- describe '#can_override_max_size?' do
- context 'when the blob size is larger than the overridable max size' do
- context 'when the blob size is larger than the max size' do
- let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
-
- it 'returns false' do
- expect(viewer.can_override_max_size?).to be_falsey
- end
- end
-
- context 'when the blob size is smaller than the max size' do
- let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
-
- it 'returns true' do
- expect(viewer.can_override_max_size?).to be_truthy
- end
- end
- end
-
- context 'when the blob size is smaller than the overridable max size' do
- let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
-
- it 'returns false' do
- expect(viewer.can_override_max_size?).to be_falsey
+ expect(viewer.too_large?).to be_falsey
end
end
end
describe '#render_error' do
- context 'when the max size is overridden' do
+ context 'when expanded' do
before do
- viewer.override_max_size = true
+ viewer.expanded = true
end
- context 'when the blob size is larger than the max size' do
+ context 'when the blob size is larger than the size limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.megabytes) }
it 'returns :too_large' do
@@ -147,7 +119,7 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the blob size is smaller than the max size' do
+ context 'when the blob size is smaller than the size limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
it 'returns nil' do
@@ -156,16 +128,16 @@ describe BlobViewer::Base, model: true do
end
end
- context 'when the max size is not overridden' do
- context 'when the blob size is larger than the overridable max size' do
+ context 'when not expanded' do
+ context 'when the blob size is larger than the collapse limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 2.megabytes) }
- it 'returns :too_large' do
- expect(viewer.render_error).to eq(:too_large)
+ it 'returns :collapsed' do
+ expect(viewer.render_error).to eq(:collapsed)
end
end
- context 'when the blob size is smaller than the overridable max size' do
+ context 'when the blob size is smaller than the collapse limit' do
let(:blob) { fake_blob(path: 'file.pdf', size: 10.kilobytes) }
it 'returns nil' do
diff --git a/spec/models/ci/build_spec.rb b/spec/models/ci/build_spec.rb
index e971b4bc3f9..e2406290c6c 100644
--- a/spec/models/ci/build_spec.rb
+++ b/spec/models/ci/build_spec.rb
@@ -1215,16 +1215,49 @@ describe Ci::Build, :models do
it { is_expected.to include(tag_variable) }
end
- context 'when secure variable is defined' do
- let(:secure_variable) do
+ context 'when secret variable is defined' do
+ let(:secret_variable) do
{ key: 'SECRET_KEY', value: 'secret_value', public: false }
end
before do
- build.project.variables << Ci::Variable.new(key: 'SECRET_KEY', value: 'secret_value')
+ create(:ci_variable,
+ secret_variable.slice(:key, :value).merge(project: project))
end
- it { is_expected.to include(secure_variable) }
+ it { is_expected.to include(secret_variable) }
+ end
+
+ context 'when protected variable is defined' do
+ let(:protected_variable) do
+ { key: 'PROTECTED_KEY', value: 'protected_value', public: false }
+ end
+
+ before do
+ create(:ci_variable,
+ :protected,
+ protected_variable.slice(:key, :value).merge(project: project))
+ end
+
+ context 'when the branch is protected' do
+ before do
+ create(:protected_branch, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the tag is protected' do
+ before do
+ create(:protected_tag, project: build.project, name: build.ref)
+ end
+
+ it { is_expected.to include(protected_variable) }
+ end
+
+ context 'when the ref is not protected' do
+ it { is_expected.not_to include(protected_variable) }
+ end
end
context 'when build is for triggers' do
@@ -1346,15 +1379,30 @@ describe Ci::Build, :models do
end
context 'returns variables in valid order' do
+ let(:build_pre_var) { { key: 'build', value: 'value' } }
+ let(:project_pre_var) { { key: 'project', value: 'value' } }
+ let(:pipeline_pre_var) { { key: 'pipeline', value: 'value' } }
+ let(:build_yaml_var) { { key: 'yaml', value: 'value' } }
+
before do
- allow(build).to receive(:predefined_variables) { ['predefined'] }
- allow(project).to receive(:predefined_variables) { ['project'] }
- allow(pipeline).to receive(:predefined_variables) { ['pipeline'] }
- allow(build).to receive(:yaml_variables) { ['yaml'] }
- allow(project).to receive(:secret_variables) { ['secret'] }
+ allow(build).to receive(:predefined_variables) { [build_pre_var] }
+ allow(project).to receive(:predefined_variables) { [project_pre_var] }
+ allow(pipeline).to receive(:predefined_variables) { [pipeline_pre_var] }
+ allow(build).to receive(:yaml_variables) { [build_yaml_var] }
+
+ allow(project).to receive(:secret_variables_for).with(build.ref) do
+ [create(:ci_variable, key: 'secret', value: 'value')]
+ end
end
- it { is_expected.to eq(%w[predefined project pipeline yaml secret]) }
+ it do
+ is_expected.to eq(
+ [build_pre_var,
+ project_pre_var,
+ pipeline_pre_var,
+ build_yaml_var,
+ { key: 'secret', value: 'value', public: false }])
+ end
end
end
diff --git a/spec/models/ci/variable_spec.rb b/spec/models/ci/variable_spec.rb
index fe8c52d5353..077b10227d7 100644
--- a/spec/models/ci/variable_spec.rb
+++ b/spec/models/ci/variable_spec.rb
@@ -12,11 +12,33 @@ describe Ci::Variable, models: true do
it { is_expected.not_to allow_value('foo bar').for(:key) }
it { is_expected.not_to allow_value('foo/bar').for(:key) }
- before :each do
- subject.value = secret_value
+ describe '.unprotected' do
+ subject { described_class.unprotected }
+
+ context 'when variable is protected' do
+ before do
+ create(:ci_variable, :protected)
+ end
+
+ it 'returns nothing' do
+ is_expected.to be_empty
+ end
+ end
+
+ context 'when variable is not protected' do
+ let(:variable) { create(:ci_variable, protected: false) }
+
+ it 'returns the variable' do
+ is_expected.to contain_exactly(variable)
+ end
+ end
end
describe '#value' do
+ before do
+ subject.value = secret_value
+ end
+
it 'stores the encrypted value' do
expect(subject.encrypted_value).not_to be_nil
end
@@ -36,4 +58,11 @@ describe Ci::Variable, models: true do
to raise_error(OpenSSL::Cipher::CipherError, 'bad decrypt')
end
end
+
+ describe '#to_runner_variable' do
+ it 'returns a hash for the runner' do
+ expect(subject.to_runner_variable)
+ .to eq(key: subject.key, value: subject.value, public: false)
+ end
+ end
end
diff --git a/spec/models/deployment_spec.rb b/spec/models/deployment_spec.rb
index 4bda7d4314a..6f0d2db23c7 100644
--- a/spec/models/deployment_spec.rb
+++ b/spec/models/deployment_spec.rb
@@ -16,6 +16,19 @@ describe Deployment, models: true do
it { is_expected.to validate_presence_of(:ref) }
it { is_expected.to validate_presence_of(:sha) }
+ describe 'after_create callbacks' do
+ let(:environment) { create(:environment) }
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'invalidates the environment etag cache' do
+ old_value = store.get(environment.etag_cache_key)
+
+ create(:deployment, environment: environment)
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#includes_commit?' do
let(:project) { create(:project, :repository) }
let(:environment) { create(:environment, project: project) }
diff --git a/spec/models/diff_note_spec.rb b/spec/models/diff_note_spec.rb
index 96f075d4f7d..297c2108dc2 100644
--- a/spec/models/diff_note_spec.rb
+++ b/spec/models/diff_note_spec.rb
@@ -160,12 +160,6 @@ describe DiffNote, models: true do
context "when noteable is a commit" do
let(:diff_note) { create(:diff_note_on_commit, project: project, position: position) }
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -178,12 +172,6 @@ describe DiffNote, models: true do
let(:diff_note) { create(:diff_note_on_merge_request, project: project, position: position, noteable: merge_request) }
context "when the note is active" do
- it "doesn't use the DiffPositionUpdateService" do
- expect(Notes::DiffPositionUpdateService).not_to receive(:new)
-
- diff_note
- end
-
it "doesn't update the position" do
diff_note
@@ -197,18 +185,11 @@ describe DiffNote, models: true do
allow(merge_request).to receive(:diff_refs).and_return(commit.diff_refs)
end
- it "uses the DiffPositionUpdateService" do
- service = instance_double("Notes::DiffPositionUpdateService")
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
- project,
- nil,
- old_diff_refs: position.diff_refs,
- new_diff_refs: commit.diff_refs,
- paths: [path]
- ).and_return(service)
- expect(service).to receive(:execute)
-
+ it "updates the position" do
diff_note
+
+ expect(diff_note.original_position).to eq(position)
+ expect(diff_note.position).not_to eq(position)
end
end
end
diff --git a/spec/models/environment_spec.rb b/spec/models/environment_spec.rb
index 9fbe19b04d5..fe69c8e351d 100644
--- a/spec/models/environment_spec.rb
+++ b/spec/models/environment_spec.rb
@@ -1,7 +1,7 @@
require 'spec_helper'
describe Environment, models: true do
- let(:project) { create(:empty_project) }
+ set(:project) { create(:empty_project) }
subject(:environment) { create(:environment, project: project) }
it { is_expected.to belong_to(:project) }
@@ -34,6 +34,26 @@ describe Environment, models: true do
end
end
+ describe 'state machine' do
+ it 'invalidates the cache after a change' do
+ expect(environment).to receive(:expire_etag_cache)
+
+ environment.stop
+ end
+ end
+
+ describe '#expire_etag_cache' do
+ let(:store) { Gitlab::EtagCaching::Store.new }
+
+ it 'changes the cached value' do
+ old_value = store.get(environment.etag_cache_key)
+
+ environment.stop
+
+ expect(store.get(environment.etag_cache_key)).not_to eq(old_value)
+ end
+ end
+
describe '#nullify_external_url' do
it 'replaces a blank url with nil' do
env = build(:environment, external_url: "")
diff --git a/spec/models/merge_request_spec.rb b/spec/models/merge_request_spec.rb
index 712470d6bf5..060754fab63 100644
--- a/spec/models/merge_request_spec.rb
+++ b/spec/models/merge_request_spec.rb
@@ -238,10 +238,10 @@ describe MergeRequest, models: true do
end
context 'when there are no MR diffs' do
- it 'delegates to the compare object, setting no_collapse: true' do
+ it 'delegates to the compare object, setting expanded: true' do
merge_request.compare = double(:compare)
- expect(merge_request.compare).to receive(:diffs).with(options.merge(no_collapse: true))
+ expect(merge_request.compare).to receive(:diffs).with(options.merge(expanded: true))
merge_request.diffs(options)
end
@@ -1178,7 +1178,7 @@ describe MergeRequest, models: true do
end
describe "#reload_diff" do
- let(:note) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: subject.project, noteable: subject).to_discussion }
let(:commit) { subject.project.commit(sample_commit.id) }
@@ -1197,7 +1197,7 @@ describe MergeRequest, models: true do
subject.reload_diff
end
- it "updates diff note positions" do
+ it "updates diff discussion positions" do
old_diff_refs = subject.diff_refs
# Update merge_request_diff so that #diff_refs will return commit.diff_refs
@@ -1211,15 +1211,15 @@ describe MergeRequest, models: true do
subject.merge_request_diff(true)
end
- expect(Notes::DiffPositionUpdateService).to receive(:new).with(
+ expect(Discussions::UpdateDiffPositionService).to receive(:new).with(
subject.project,
subject.author,
old_diff_refs: old_diff_refs,
new_diff_refs: commit.diff_refs,
- paths: note.position.paths
+ paths: discussion.position.paths
).and_call_original
- expect_any_instance_of(Notes::DiffPositionUpdateService).to receive(:execute).with(note)
+ expect_any_instance_of(Discussions::UpdateDiffPositionService).to receive(:execute).with(discussion).and_call_original
expect_any_instance_of(DiffNote).to receive(:save).once
subject.reload_diff(subject.author)
diff --git a/spec/models/project_services/jira_service_spec.rb b/spec/models/project_services/jira_service_spec.rb
index 1920b5bf42b..0ee050196e4 100644
--- a/spec/models/project_services/jira_service_spec.rb
+++ b/spec/models/project_services/jira_service_spec.rb
@@ -69,41 +69,6 @@ describe JiraService, models: true do
end
end
- describe '#can_test?' do
- let(:jira_service) { described_class.new }
-
- it 'returns false if username is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: '',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns false if password is blank' do
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: ''
- )
-
- expect(jira_service.can_test?).to be_falsy
- end
-
- it 'returns true if password and username are present' do
- jira_service = described_class.new
- allow(jira_service).to receive_messages(
- url: 'http://jira.example.com',
- username: 'tester',
- password: '12345678'
- )
-
- expect(jira_service.can_test?).to be_truthy
- end
- end
-
describe '#close_issue' do
let(:custom_base_url) { 'http://custom_url' }
let(:user) { create(:user) }
diff --git a/spec/models/project_spec.rb b/spec/models/project_spec.rb
index da1b29a2bda..86ab2550bfb 100644
--- a/spec/models/project_spec.rb
+++ b/spec/models/project_spec.rb
@@ -1749,6 +1749,90 @@ describe Project, models: true do
end
end
+ describe '#secret_variables_for' do
+ let(:project) { create(:empty_project) }
+
+ let!(:secret_variable) do
+ create(:ci_variable, value: 'secret', project: project)
+ end
+
+ let!(:protected_variable) do
+ create(:ci_variable, :protected, value: 'protected', project: project)
+ end
+
+ subject { project.secret_variables_for('ref') }
+
+ shared_examples 'ref is protected' do
+ it 'contains all the variables' do
+ is_expected.to contain_exactly(secret_variable, protected_variable)
+ end
+ end
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'contains only the secret variables' do
+ is_expected.to contain_exactly(secret_variable)
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it_behaves_like 'ref is protected'
+ end
+ end
+
+ describe '#protected_for?' do
+ let(:project) { create(:empty_project) }
+
+ subject { project.protected_for?('ref') }
+
+ context 'when the ref is not protected' do
+ before do
+ stub_application_setting(
+ default_branch_protection: Gitlab::Access::PROTECTION_NONE)
+ end
+
+ it 'returns false' do
+ is_expected.to be_falsey
+ end
+ end
+
+ context 'when the ref is a protected branch' do
+ before do
+ create(:protected_branch, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+
+ context 'when the ref is a protected tag' do
+ before do
+ create(:protected_tag, name: 'ref', project: project)
+ end
+
+ it 'returns true' do
+ is_expected.to be_truthy
+ end
+ end
+ end
+
describe '#update_project_statistics' do
let(:project) { create(:empty_project) }
diff --git a/spec/models/project_team_spec.rb b/spec/models/project_team_spec.rb
index fb2d5f60009..362565506e5 100644
--- a/spec/models/project_team_spec.rb
+++ b/spec/models/project_team_spec.rb
@@ -327,69 +327,114 @@ describe ProjectTeam, models: true do
end
end
- shared_examples_for "#max_member_access_for_users" do |enable_request_store|
- describe "#max_member_access_for_users" do
+ shared_examples 'max member access for users' do
+ let(:project) { create(:project) }
+ let(:group) { create(:group) }
+ let(:second_group) { create(:group) }
+
+ let(:master) { create(:user) }
+ let(:reporter) { create(:user) }
+ let(:guest) { create(:user) }
+
+ let(:promoted_guest) { create(:user) }
+
+ let(:group_developer) { create(:user) }
+ let(:second_developer) { create(:user) }
+
+ let(:user_without_access) { create(:user) }
+ let(:second_user_without_access) { create(:user) }
+
+ let(:users) do
+ [master, reporter, promoted_guest, guest, group_developer, second_developer, user_without_access].map(&:id)
+ end
+
+ let(:expected) do
+ {
+ master.id => Gitlab::Access::MASTER,
+ reporter.id => Gitlab::Access::REPORTER,
+ promoted_guest.id => Gitlab::Access::DEVELOPER,
+ guest.id => Gitlab::Access::GUEST,
+ group_developer.id => Gitlab::Access::DEVELOPER,
+ second_developer.id => Gitlab::Access::MASTER,
+ user_without_access.id => Gitlab::Access::NO_ACCESS
+ }
+ end
+
+ before do
+ project.add_master(master)
+ project.add_reporter(reporter)
+ project.add_guest(promoted_guest)
+ project.add_guest(guest)
+
+ project.project_group_links.create(
+ group: group,
+ group_access: Gitlab::Access::DEVELOPER
+ )
+
+ group.add_master(promoted_guest)
+ group.add_developer(group_developer)
+ group.add_developer(second_developer)
+
+ project.project_group_links.create(
+ group: second_group,
+ group_access: Gitlab::Access::MASTER
+ )
+
+ second_group.add_master(second_developer)
+ end
+
+ it 'returns correct roles for different users' do
+ expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ end
+ end
+
+ describe '#max_member_access_for_user_ids' do
+ context 'with RequestStore enabled' do
before do
- RequestStore.begin! if enable_request_store
+ RequestStore.begin!
end
after do
- if enable_request_store
- RequestStore.end!
- RequestStore.clear!
- end
+ RequestStore.end!
+ RequestStore.clear!
end
- it 'returns correct roles for different users' do
- master = create(:user)
- reporter = create(:user)
- promoted_guest = create(:user)
- guest = create(:user)
- project = create(:empty_project)
+ include_examples 'max member access for users'
- project.add_master(master)
- project.add_reporter(reporter)
- project.add_guest(promoted_guest)
- project.add_guest(guest)
+ def access_levels(users)
+ project.team.max_member_access_for_user_ids(users)
+ end
+
+ it 'does not perform extra queries when asked for users who have already been found' do
+ access_levels(users)
+
+ expect { access_levels(users) }.not_to exceed_query_limit(0)
- group = create(:group)
- group_developer = create(:user)
- second_developer = create(:user)
- project.project_group_links.create(
- group: group,
- group_access: Gitlab::Access::DEVELOPER)
-
- group.add_master(promoted_guest)
- group.add_developer(group_developer)
- group.add_developer(second_developer)
-
- second_group = create(:group)
- project.project_group_links.create(
- group: second_group,
- group_access: Gitlab::Access::MASTER)
- second_group.add_master(second_developer)
-
- users = [master, reporter, promoted_guest, guest, group_developer, second_developer].map(&:id)
-
- expected = {
- master.id => Gitlab::Access::MASTER,
- reporter.id => Gitlab::Access::REPORTER,
- promoted_guest.id => Gitlab::Access::DEVELOPER,
- guest.id => Gitlab::Access::GUEST,
- group_developer.id => Gitlab::Access::DEVELOPER,
- second_developer.id => Gitlab::Access::MASTER
- }
-
- expect(project.team.max_member_access_for_user_ids(users)).to eq(expected)
+ expect(access_levels(users)).to eq(expected)
end
- end
- end
- describe '#max_member_access_for_users with RequestStore' do
- it_behaves_like "#max_member_access_for_users", true
- end
+ it 'only requests the extra users when uncached users are passed' do
+ new_user = create(:user)
+ second_new_user = create(:user)
+ all_users = users + [new_user.id, second_new_user.id]
+
+ expected_all = expected.merge(new_user.id => Gitlab::Access::NO_ACCESS,
+ second_new_user.id => Gitlab::Access::NO_ACCESS)
+
+ access_levels(users)
- describe '#max_member_access_for_users without RequestStore' do
- it_behaves_like "#max_member_access_for_users", false
+ queries = ActiveRecord::QueryRecorder.new { access_levels(all_users) }
+
+ expect(queries.count).to eq(1)
+ expect(queries.log_message).to match(/\W#{new_user.id}\W/)
+ expect(queries.log_message).to match(/\W#{second_new_user.id}\W/)
+ expect(queries.log_message).not_to match(/\W#{promoted_guest.id}\W/)
+ expect(access_levels(all_users)).to eq(expected_all)
+ end
+ end
+
+ context 'with RequestStore disabled' do
+ include_examples 'max member access for users'
+ end
end
end
diff --git a/spec/models/user_spec.rb b/spec/models/user_spec.rb
index fe9df3360ff..1c3541da44f 100644
--- a/spec/models/user_spec.rb
+++ b/spec/models/user_spec.rb
@@ -22,7 +22,7 @@ describe User, models: true do
it { is_expected.to have_many(:deploy_keys).dependent(:destroy) }
it { is_expected.to have_many(:events).dependent(:destroy) }
it { is_expected.to have_many(:recent_events).class_name('Event') }
- it { is_expected.to have_many(:issues).dependent(:restrict_with_exception) }
+ it { is_expected.to have_many(:issues).dependent(:destroy) }
it { is_expected.to have_many(:notes).dependent(:destroy) }
it { is_expected.to have_many(:merge_requests).dependent(:destroy) }
it { is_expected.to have_many(:identities).dependent(:destroy) }
diff --git a/spec/requests/api/projects_spec.rb b/spec/requests/api/projects_spec.rb
index 3d98551628b..40bfc0c636b 100644
--- a/spec/requests/api/projects_spec.rb
+++ b/spec/requests/api/projects_spec.rb
@@ -316,15 +316,15 @@ describe API::Projects do
expect(project.path).to eq('foo_project')
end
- it 'creates new project name and path and returns 201' do
- expect { post api('/projects', user), path: 'foo-Project', name: 'Foo Project' }.
+ it 'creates new project with name and path and returns 201' do
+ expect { post api('/projects', user), path: 'path-project-Foo', name: 'Foo Project' }.
to change { Project.count }.by(1)
expect(response).to have_http_status(201)
project = Project.first
expect(project.name).to eq('Foo Project')
- expect(project.path).to eq('foo-Project')
+ expect(project.path).to eq('path-project-Foo')
end
it 'creates last project before reaching project limit' do
@@ -470,9 +470,25 @@ describe API::Projects do
before { project }
before { admin }
- it 'creates new project without path and return 201' do
- expect { post api("/projects/user/#{user.id}", admin), name: 'foo' }.to change {Project.count}.by(1)
+ it 'creates new project without path but with name and return 201' do
+ expect { post api("/projects/user/#{user.id}", admin), name: 'Foo Project' }.to change {Project.count}.by(1)
expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('foo-project')
+ end
+
+ it 'creates new project with name and path and returns 201' do
+ expect { post api("/projects/user/#{user.id}", admin), path: 'path-project-Foo', name: 'Foo Project' }.
+ to change { Project.count }.by(1)
+ expect(response).to have_http_status(201)
+
+ project = Project.first
+
+ expect(project.name).to eq('Foo Project')
+ expect(project.path).to eq('path-project-Foo')
end
it 'responds with 400 on failure and not project' do
diff --git a/spec/requests/api/v3/deploy_keys_spec.rb b/spec/requests/api/v3/deploy_keys_spec.rb
index b61b2b618a6..94f4d93a8dc 100644
--- a/spec/requests/api/v3/deploy_keys_spec.rb
+++ b/spec/requests/api/v3/deploy_keys_spec.rb
@@ -105,6 +105,15 @@ describe API::V3::DeployKeys do
expect(response).to have_http_status(201)
end
+
+ it 'accepts can_push parameter' do
+ key_attrs = attributes_for :write_access_key
+
+ post v3_api("/projects/#{project.id}/#{path}", admin), key_attrs
+
+ expect(response).to have_http_status(201)
+ expect(json_response['can_push']).to eq(true)
+ end
end
describe "DELETE /projects/:id/#{path}/:key_id" do
diff --git a/spec/requests/api/variables_spec.rb b/spec/requests/api/variables_spec.rb
index 63d6d3001ac..83673864fe7 100644
--- a/spec/requests/api/variables_spec.rb
+++ b/spec/requests/api/variables_spec.rb
@@ -42,6 +42,7 @@ describe API::Variables do
expect(response).to have_http_status(200)
expect(json_response['value']).to eq(variable.value)
+ expect(json_response['protected']).to eq(variable.protected?)
end
it 'responds with 404 Not Found if requesting non-existing variable' do
@@ -72,12 +73,13 @@ describe API::Variables do
context 'authorized user with proper permissions' do
it 'creates variable' do
expect do
- post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2'
+ post api("/projects/#{project.id}/variables", user), key: 'TEST_VARIABLE_2', value: 'VALUE_2', protected: true
end.to change{project.variables.count}.by(1)
expect(response).to have_http_status(201)
expect(json_response['key']).to eq('TEST_VARIABLE_2')
expect(json_response['value']).to eq('VALUE_2')
+ expect(json_response['protected']).to be_truthy
end
it 'does not allow to duplicate variable key' do
@@ -112,13 +114,14 @@ describe API::Variables do
initial_variable = project.variables.first
value_before = initial_variable.value
- put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP'
+ put api("/projects/#{project.id}/variables/#{variable.key}", user), value: 'VALUE_1_UP', protected: true
updated_variable = project.variables.first
expect(response).to have_http_status(200)
expect(value_before).to eq(variable.value)
expect(updated_variable.value).to eq('VALUE_1_UP')
+ expect(updated_variable).to be_protected
end
it 'responds with 404 Not Found if requesting non-existing variable' do
diff --git a/spec/rubocop/cop/activerecord_serialize_spec.rb b/spec/rubocop/cop/activerecord_serialize_spec.rb
new file mode 100644
index 00000000000..a303b16d264
--- /dev/null
+++ b/spec/rubocop/cop/activerecord_serialize_spec.rb
@@ -0,0 +1,33 @@
+require 'spec_helper'
+require 'rubocop'
+require 'rubocop/rspec/support'
+require_relative '../../../rubocop/cop/activerecord_serialize'
+
+describe RuboCop::Cop::ActiverecordSerialize do
+ include CopHelper
+
+ subject(:cop) { described_class.new }
+
+ context 'inside the app/models directory' do
+ it 'registers an offense when serialize is used' do
+ allow(cop).to receive(:in_models?).and_return(true)
+
+ inspect_source(cop, 'serialize :foo')
+
+ aggregate_failures do
+ expect(cop.offenses.size).to eq(1)
+ expect(cop.offenses.map(&:line)).to eq([1])
+ end
+ end
+ end
+
+ context 'outside the app/models directory' do
+ it 'does nothing' do
+ allow(cop).to receive(:in_models?).and_return(false)
+
+ inspect_source(cop, 'serialize :foo')
+
+ expect(cop.offenses).to be_empty
+ end
+ end
+end
diff --git a/spec/serializers/pipeline_serializer_spec.rb b/spec/serializers/pipeline_serializer_spec.rb
index f2426db6d81..088f24eb180 100644
--- a/spec/serializers/pipeline_serializer_spec.rb
+++ b/spec/serializers/pipeline_serializer_spec.rb
@@ -113,7 +113,7 @@ describe PipelineSerializer do
it "verifies number of queries" do
recorded = ActiveRecord::QueryRecorder.new { subject }
- expect(recorded.count).to be_within(1).of(58)
+ expect(recorded.count).to be_within(1).of(60)
expect(recorded.cached_count).to eq(0)
end
diff --git a/spec/serializers/user_entity_spec.rb b/spec/serializers/user_entity_spec.rb
index c5d11cbcf5e..cd778e49107 100644
--- a/spec/serializers/user_entity_spec.rb
+++ b/spec/serializers/user_entity_spec.rb
@@ -1,6 +1,8 @@
require 'spec_helper'
describe UserEntity do
+ include Gitlab::Routing
+
let(:entity) { described_class.new(user) }
let(:user) { create(:user) }
subject { entity.as_json }
@@ -20,4 +22,8 @@ describe UserEntity do
it 'does not expose 2FA OTPs' do
expect(subject).not_to include(/otp/)
end
+
+ it 'exposes user path' do
+ expect(subject[:path]).to eq user_path(user)
+ end
end
diff --git a/spec/services/ci/create_pipeline_service_spec.rb b/spec/services/ci/create_pipeline_service_spec.rb
index 8bf02f56282..06fbd7bad90 100644
--- a/spec/services/ci/create_pipeline_service_spec.rb
+++ b/spec/services/ci/create_pipeline_service_spec.rb
@@ -72,10 +72,11 @@ describe Ci::CreatePipelineService, services: true do
end
end
- context 'when merge request head commit sha does not match pipeline sha' do
+ context 'when the pipeline is not the latest for the branch' do
it 'does not update merge request head pipeline' do
merge_request = create(:merge_request, source_branch: 'master', target_branch: "branch_1", source_project: project)
- allow_any_instance_of(MergeRequestDiff).to receive(:head_commit).and_return(double(id: 1234))
+
+ allow_any_instance_of(Ci::Pipeline).to receive(:latest?).and_return(false)
pipeline
diff --git a/spec/services/notes/diff_position_update_service_spec.rb b/spec/services/discussions/update_diff_position_service_spec.rb
index 380c296fd3a..177e32e13bd 100644
--- a/spec/services/notes/diff_position_update_service_spec.rb
+++ b/spec/services/discussions/update_diff_position_service_spec.rb
@@ -1,6 +1,6 @@
require 'spec_helper'
-describe Notes::DiffPositionUpdateService, services: true do
+describe Discussions::UpdateDiffPositionService, services: true do
let(:project) { create(:project, :repository) }
let(:current_user) { project.owner }
let(:create_commit) { project.commit("913c66a37b4a45b9769037c55c2d238bd0942d2e") }
@@ -138,7 +138,7 @@ describe Notes::DiffPositionUpdateService, services: true do
# .. ..
describe "#execute" do
- let(:note) { create(:diff_note_on_merge_request, project: project, position: old_position) }
+ let(:discussion) { create(:diff_note_on_merge_request, project: project, position: old_position).to_discussion }
let(:old_position) do
Gitlab::Diff::Position.new(
@@ -154,11 +154,11 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 16 }
it "updates the position" do
- subject.execute(note)
+ subject.execute(discussion)
- expect(note.original_position).to eq(old_position)
- expect(note.position).not_to eq(old_position)
- expect(note.position.new_line).to eq(22)
+ expect(discussion.original_position).to eq(old_position)
+ expect(discussion.position).not_to eq(old_position)
+ expect(discussion.position.new_line).to eq(22)
end
end
@@ -166,27 +166,27 @@ describe Notes::DiffPositionUpdateService, services: true do
let(:line) { 9 }
it "doesn't update the position" do
- subject.execute(note)
+ subject.execute(discussion)
- expect(note.original_position).to eq(old_position)
- expect(note.position).to eq(old_position)
+ expect(discussion.original_position).to eq(old_position)
+ expect(discussion.position).to eq(old_position)
end
it 'sets the change position' do
- subject.execute(note)
+ subject.execute(discussion)
- change_position = note.change_position
+ change_position = discussion.change_position
expect(change_position.start_sha).to eq(old_diff_refs.head_sha)
expect(change_position.head_sha).to eq(new_diff_refs.head_sha)
expect(change_position.old_line).to eq(9)
expect(change_position.new_line).to be_nil
end
- it 'creates a system note' do
+ it 'creates a system discussion' do
expect(SystemNoteService).to receive(:diff_discussion_outdated).with(
- note.to_discussion, project, current_user, instance_of(Gitlab::Diff::Position))
+ discussion, project, current_user, instance_of(Gitlab::Diff::Position))
- subject.execute(note)
+ subject.execute(discussion)
end
end
end
diff --git a/spec/services/gravatar_service_spec.rb b/spec/services/gravatar_service_spec.rb
new file mode 100644
index 00000000000..8c4ad8c7a3e
--- /dev/null
+++ b/spec/services/gravatar_service_spec.rb
@@ -0,0 +1,20 @@
+require 'spec_helper'
+
+describe GravatarService, service: true do
+ describe '#execute' do
+ let(:url) { 'http://example.com/avatar?hash=%{hash}&size=%{size}&email=%{email}&username=%{username}' }
+
+ before do
+ allow(Gitlab.config.gravatar).to receive(:plain_url).and_return(url)
+ end
+
+ it 'replaces the placeholders' do
+ avatar_url = described_class.new.execute('user@example.com', 100, 2, username: 'user')
+
+ expect(avatar_url).to include("hash=#{Digest::MD5.hexdigest('user@example.com')}")
+ expect(avatar_url).to include("size=200")
+ expect(avatar_url).to include("email=user%40example.com")
+ expect(avatar_url).to include("username=user")
+ end
+ end
+end
diff --git a/spec/services/merge_requests/conflicts/resolve_service_spec.rb b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
index 19e8d5cc5f1..c77e6e9cd50 100644
--- a/spec/services/merge_requests/conflicts/resolve_service_spec.rb
+++ b/spec/services/merge_requests/conflicts/resolve_service_spec.rb
@@ -26,6 +26,10 @@ describe MergeRequests::Conflicts::ResolveService do
describe '#execute' do
let(:service) { described_class.new(merge_request) }
+ def blob_content(project, ref, path)
+ project.repository.blob_at(ref, path).data
+ end
+
context 'with section params' do
let(:params) do
{
@@ -66,6 +70,35 @@ describe MergeRequests::Conflicts::ResolveService do
end
end
+ context 'when some files have trailing newlines' do
+ let!(:source_head) do
+ branch = 'conflict-resolvable'
+ path = 'files/ruby/popen.rb'
+ popen_content = blob_content(project, branch, path)
+
+ project.repository.update_file(
+ user,
+ path,
+ popen_content.chomp("\n"),
+ message: 'Remove trailing newline from popen.rb',
+ branch_name: branch
+ )
+ end
+
+ before do
+ service.execute(user, params)
+ end
+
+ it 'preserves trailing newlines from our side of the conflicts' do
+ head_sha = merge_request.source_branch_head.sha
+ popen_content = blob_content(project, head_sha, 'files/ruby/popen.rb')
+ regex_content = blob_content(project, head_sha, 'files/ruby/regex.rb')
+
+ expect(popen_content).not_to end_with("\n")
+ expect(regex_content).to end_with("\n")
+ end
+ end
+
context 'when the source project is a fork and does not contain the HEAD of the target branch' do
let!(:target_head) do
project.repository.create_file(
@@ -142,10 +175,13 @@ describe MergeRequests::Conflicts::ResolveService do
end
it 'sets the content to the content given' do
- blob = merge_request.source_project.repository.blob_at(merge_request.source_branch_head.sha,
- 'files/ruby/popen.rb')
+ blob = blob_content(
+ merge_request.source_project,
+ merge_request.source_branch_head.sha,
+ 'files/ruby/popen.rb'
+ )
- expect(blob.data).to eq(popen_content)
+ expect(blob).to eq(popen_content)
end
end
diff --git a/spec/services/users/destroy_service_spec.rb b/spec/services/users/destroy_service_spec.rb
index de37a61e388..5409f67c091 100644
--- a/spec/services/users/destroy_service_spec.rb
+++ b/spec/services/users/destroy_service_spec.rb
@@ -147,16 +147,22 @@ describe Users::DestroyService, services: true do
end
context "migrating associated records" do
+ let!(:issue) { create(:issue, author: user) }
+
it 'delegates to the `MigrateToGhostUser` service to move associated records to the ghost user' do
- expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once
+ expect_any_instance_of(Users::MigrateToGhostUserService).to receive(:execute).once.and_call_original
service.execute(user)
+
+ expect(issue.reload.author).to be_ghost
end
it 'does not run `MigrateToGhostUser` if hard_delete option is given' do
expect_any_instance_of(Users::MigrateToGhostUserService).not_to receive(:execute)
service.execute(user, hard_delete: true)
+
+ expect(Issue.exists?(issue.id)).to be_falsy
end
end
end
diff --git a/spec/services/wiki_pages/create_service_spec.rb b/spec/services/wiki_pages/create_service_spec.rb
index 5341ba3d261..054e28ae7b0 100644
--- a/spec/services/wiki_pages/create_service_spec.rb
+++ b/spec/services/wiki_pages/create_service_spec.rb
@@ -3,6 +3,7 @@ require 'spec_helper'
describe WikiPages::CreateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
+
let(:opts) do
{
title: 'Title',
@@ -10,27 +11,28 @@ describe WikiPages::CreateService, services: true do
format: 'markdown'
}
end
- let(:service) { described_class.new(project, user, opts) }
+
+ subject(:service) { described_class.new(project, user, opts) }
+
+ before do
+ project.add_developer(user)
+ end
describe '#execute' do
- context "valid params" do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
-
- subject { service.execute }
-
- it 'creates a valid wiki page' do
- is_expected.to be_valid
- expect(subject.title).to eq(opts[:title])
- expect(subject.content).to eq(opts[:content])
- expect(subject.format).to eq(opts[:format].to_sym)
- end
-
- it 'executes webhooks' do
- expect(service).to have_received(:execute_hooks).once.with(subject, 'create')
- end
+ it 'creates wiki page with valid attributes' do
+ page = service.execute
+
+ expect(page).to be_valid
+ expect(page.title).to eq(opts[:title])
+ expect(page.content).to eq(opts[:content])
+ expect(page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'create')
+
+ service.execute
end
end
end
diff --git a/spec/services/wiki_pages/destroy_service_spec.rb b/spec/services/wiki_pages/destroy_service_spec.rb
index a4b9a390fe2..920be4d4c8a 100644
--- a/spec/services/wiki_pages/destroy_service_spec.rb
+++ b/spec/services/wiki_pages/destroy_service_spec.rb
@@ -3,19 +3,20 @@ require 'spec_helper'
describe WikiPages::DestroyService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:wiki_page) { create(:wiki_page) }
- let(:service) { described_class.new(project, user) }
+ let(:page) { create(:wiki_page) }
- describe '#execute' do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
+ subject(:service) { described_class.new(project, user) }
+ before do
+ project.add_developer(user)
+ end
+
+ describe '#execute' do
it 'executes webhooks' do
- service.execute(wiki_page)
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'delete')
- expect(service).to have_received(:execute_hooks).once.with(wiki_page, 'delete')
+ service.execute(page)
end
end
end
diff --git a/spec/services/wiki_pages/update_service_spec.rb b/spec/services/wiki_pages/update_service_spec.rb
index 2bccca764d7..5e36ea4cf94 100644
--- a/spec/services/wiki_pages/update_service_spec.rb
+++ b/spec/services/wiki_pages/update_service_spec.rb
@@ -3,7 +3,8 @@ require 'spec_helper'
describe WikiPages::UpdateService, services: true do
let(:project) { create(:empty_project) }
let(:user) { create(:user) }
- let(:wiki_page) { create(:wiki_page) }
+ let(:page) { create(:wiki_page) }
+
let(:opts) do
{
content: 'New content for wiki page',
@@ -11,27 +12,28 @@ describe WikiPages::UpdateService, services: true do
message: 'New wiki message'
}
end
- let(:service) { described_class.new(project, user, opts) }
+
+ subject(:service) { described_class.new(project, user, opts) }
+
+ before do
+ project.add_developer(user)
+ end
describe '#execute' do
- context "valid params" do
- before do
- allow(service).to receive(:execute_hooks)
- project.add_master(user)
- end
-
- subject { service.execute(wiki_page) }
-
- it 'updates the wiki page' do
- is_expected.to be_valid
- expect(subject.content).to eq(opts[:content])
- expect(subject.format).to eq(opts[:format].to_sym)
- expect(subject.message).to eq(opts[:message])
- end
-
- it 'executes webhooks' do
- expect(service).to have_received(:execute_hooks).once.with(subject, 'update')
- end
+ it 'updates the wiki page' do
+ updated_page = service.execute(page)
+
+ expect(updated_page).to be_valid
+ expect(updated_page.message).to eq(opts[:message])
+ expect(updated_page.content).to eq(opts[:content])
+ expect(updated_page.format).to eq(opts[:format].to_sym)
+ end
+
+ it 'executes webhooks' do
+ expect(service).to receive(:execute_hooks).once
+ .with(instance_of(WikiPage), 'update')
+
+ service.execute(page)
end
end
end
diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb
index 4c2eba8fa46..994c7dcbb46 100644
--- a/spec/spec_helper.rb
+++ b/spec/spec_helper.rb
@@ -26,6 +26,9 @@ if ENV['CI'] && !ENV['NO_KNAPSACK']
Knapsack::Adapters::RSpecAdapter.bind
end
+# require rainbow gem String monkeypatch, so we can test SystemChecks
+require 'rainbow/ext/string'
+
# Requires supporting ruby files with custom matchers and macros, etc,
# in spec/support/ and its subdirectories.
Dir[Rails.root.join("spec/support/**/*.rb")].each { |f| require f }
diff --git a/spec/support/matchers/execute_check.rb b/spec/support/matchers/execute_check.rb
new file mode 100644
index 00000000000..7232fad52fb
--- /dev/null
+++ b/spec/support/matchers/execute_check.rb
@@ -0,0 +1,23 @@
+RSpec::Matchers.define :execute_check do |expected|
+ match do |actual|
+ expect(actual).to eq(SystemCheck)
+ expect(actual).to receive(:run) do |*args|
+ expect(args[1]).to include(expected)
+ end
+ end
+
+ match_when_negated do |actual|
+ expect(actual).to eq(SystemCheck)
+ expect(actual).to receive(:run) do |*args|
+ expect(args[1]).not_to include(expected)
+ end
+ end
+
+ failure_message do |actual|
+ 'This matcher must be used with SystemCheck' unless actual == SystemCheck
+ end
+
+ failure_message_when_negated do |actual|
+ 'This matcher must be used with SystemCheck' unless actual == SystemCheck
+ end
+end
diff --git a/spec/support/rake_helpers.rb b/spec/support/rake_helpers.rb
index 4a8158ed79b..5cb415111d2 100644
--- a/spec/support/rake_helpers.rb
+++ b/spec/support/rake_helpers.rb
@@ -7,4 +7,9 @@ module RakeHelpers
def stub_warn_user_is_not_gitlab
allow_any_instance_of(Object).to receive(:warn_user_is_not_gitlab)
end
+
+ def silence_output
+ allow($stdout).to receive(:puts)
+ allow($stdout).to receive(:print)
+ end
end
diff --git a/spec/support/test_env.rb b/spec/support/test_env.rb
index 3c000feba5d..72b3b226c1e 100644
--- a/spec/support/test_env.rb
+++ b/spec/support/test_env.rb
@@ -40,7 +40,7 @@ module TestEnv
'wip' => 'b9238ee',
'csv' => '3dd0896',
'v1.1.0' => 'b83d6e3',
- 'add-ipython-files' => '6d85bb6',
+ 'add-ipython-files' => '93ee732',
'add-pdf-file' => 'e774ebd'
}.freeze
diff --git a/spec/uploaders/artifact_uploader_spec.rb b/spec/uploaders/artifact_uploader_spec.rb
new file mode 100644
index 00000000000..24e2e3a9f0e
--- /dev/null
+++ b/spec/uploaders/artifact_uploader_spec.rb
@@ -0,0 +1,38 @@
+require 'rails_helper'
+
+describe ArtifactUploader do
+ let(:job) { create(:ci_build) }
+ let(:uploader) { described_class.new(job, :artifacts_file) }
+ let(:path) { Gitlab.config.artifacts.path }
+
+ describe '.local_artifacts_store' do
+ subject { described_class.local_artifacts_store }
+
+ it "delegate to artifacts path" do
+ expect(Gitlab.config.artifacts).to receive(:path)
+
+ subject
+ end
+ end
+
+ describe '.artifacts_upload_path' do
+ subject { described_class.artifacts_upload_path }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('tmp/uploads/') }
+ end
+
+ describe '#store_dir' do
+ subject { uploader.store_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with("#{job.project_id}/#{job.id}") }
+ end
+
+ describe '#cache_dir' do
+ subject { uploader.cache_dir }
+
+ it { is_expected.to start_with(path) }
+ it { is_expected.to end_with('tmp/cache') }
+ end
+end
diff --git a/spec/uploaders/gitlab_uploader_spec.rb b/spec/uploaders/gitlab_uploader_spec.rb
new file mode 100644
index 00000000000..78e9d9cf46c
--- /dev/null
+++ b/spec/uploaders/gitlab_uploader_spec.rb
@@ -0,0 +1,56 @@
+require 'rails_helper'
+require 'carrierwave/storage/fog'
+
+describe GitlabUploader do
+ let(:uploader_class) { Class.new(described_class) }
+
+ subject { uploader_class.new }
+
+ describe '#file_storage?' do
+ context 'when file storage is used' do
+ before do
+ uploader_class.storage(:file)
+ end
+
+ it { is_expected.to be_file_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ uploader_class.storage(:fog)
+ end
+
+ it { is_expected.not_to be_file_storage }
+ end
+ end
+
+ describe '#file_cache_storage?' do
+ context 'when file storage is used' do
+ before do
+ uploader_class.cache_storage(:file)
+ end
+
+ it { is_expected.to be_file_cache_storage }
+ end
+
+ context 'when is remote storage' do
+ before do
+ uploader_class.cache_storage(:fog)
+ end
+
+ it { is_expected.not_to be_file_cache_storage }
+ end
+ end
+
+ describe '#move_to_cache' do
+ it 'is true' do
+ expect(subject.move_to_cache).to eq(true)
+ end
+ end
+
+ describe '#move_to_store' do
+ it 'is true' do
+ expect(subject.move_to_store).to eq(true)
+ end
+ end
+end
diff --git a/spec/views/projects/blob/_viewer.html.haml_spec.rb b/spec/views/projects/blob/_viewer.html.haml_spec.rb
index c6b0ed8da3c..bbd7f98fa8d 100644
--- a/spec/views/projects/blob/_viewer.html.haml_spec.rb
+++ b/spec/views/projects/blob/_viewer.html.haml_spec.rb
@@ -10,8 +10,8 @@ describe 'projects/blob/_viewer.html.haml', :view do
include BlobViewer::Rich
self.partial_name = 'text'
- self.overridable_max_size = 1.megabyte
- self.max_size = 5.megabytes
+ self.collapse_limit = 1.megabyte
+ self.size_limit = 5.megabytes
self.load_async = true
end
end